🎉 New release for Pusher Chatkit - Webhooks! Extend your in-app chat functionality
Hide
Products
chatkit_full-logo

Extensible API for in-app chat

channels_full-logo

Build scalable realtime features

beams_full-logo

Programmatic push notifications

Developers

Docs

Read the docs to learn how to use our products

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Sign in
Sign up

Building a chatbot for Android with Kotlin and Dialogflow

  • Esteban Herrera
September 19th, 2018
You will need an Android development environment set up on your machine, including Java JDK 8+ and Gradle 4.7+. You will also need ngrok, git and a Google account. This tutorial assumes some familiarity with Android development.

In a previous tutorial, I showed you how to create a chat app for Android using Kotlin and Pusher.

In this tutorial, you’ll learn how to extend that chat to integrate a chatbot that gives trivia about numbers:

The app will use Dialogflow to process the language of the user and understand what they are saying. It will call the Numbers API to get random facts about a number.

Under the hood, the app communicates to a REST API (also implemented in Kotlin) that publishes the message to Pusher. If the message is directed to the bot, it calls Dialogflow's API to get the bot's response.

In turn, Dialogflow will process the message to get the user's intent and extract the number for the trivia. Then, it will call an endpoint of the REST API that makes the actual request to the Numbers API to get the trivia.

Here’s the diagram that describes the above process:

For reference, the entire source code for the application is on GitHub.

Prerequisites

Here’s what you need to have installed/configured to follow this tutorial:

  • Java JDK (8 or superior)
  • Gradle (4.7 or superior)
  • The latest version of Android Studio (at the time of this writing 3.1.4)
  • Two Android emulators or two devices to test the app
  • A Google account for signing in to Dialogflow
  • ngrok, so Dialogflow can access the endpoint on the server API
  • Optionally, a Java IDE with Kotlin support, like IntelliJ IDEA Community Edition

I also assume that you are familiar with:

  • Android development (an upper-beginner level at least)
  • Kotlin
  • Android Studio

Let’s get started.

Creating a Pusher application

Create a free account at Pusher.

Then, go to your dashboard and create a Channels app, choosing a name, the cluster closest to your location, and optionally, Android as the frontend tech and Java as the backend tech.

Save your app ID, key, secret and cluster values, you’ll need them later. You can also find them in the App Keys tab.

Building the Android app

We’ll use the application from the previous tutorial as the starter project for this one. Clone it from here.

Don’t follow the steps in the README file of the repo, I’ll show you what you need to do for this app in this tutorial. If you want to know how this project was built, you can learn here.

Now, open the Android app from the starter project in Android Studio.

You can update the versions of the Kotlin plugin, Gradle, or other libraries if Android Studio ask you to.

In this project, we’re only going to add two XML files and modify two classes.

In the res/drawable directory, create a new drawable resource file, bot_message_bubble.xml, with the following content:

    <?xml version="1.0" encoding="utf-8"?>

    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">

        <solid android:color="#11de72"></solid>

        <corners  android:topLeftRadius="5dp" android:radius="40dp"></corners>

    </shape>

Next, in the res/layout directory, create a new layout resource file, bot_message.xml, for the messages of the bot:

    <!-- res/layout/bot_message.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingTop="8dp">

        <TextView
            android:id="@+id/txtBotUser"
            android:text="Trivia Bot"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="12sp"
            android:textStyle="bold"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginTop="5dp" />

        <TextView
            android:id="@+id/txtBotMessage"
            android:text="Hi, Bot's message"
            android:background="@drawable/bot_message_bubble"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:maxWidth="240dp"
            android:padding="15dp"
            android:elevation="5dp"
            android:textColor="#ffffff"
            android:layout_marginTop="4dp"
            app:layout_constraintTop_toBottomOf="@+id/txtBotUser" />

        <TextView
            android:id="@+id/txtBotMessageTime"
            android:text="12:00 PM"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="10sp"
            android:textStyle="bold"
            app:layout_constraintLeft_toRightOf="@+id/txtBotMessage"
            android:layout_marginLeft="10dp"
            app:layout_constraintBottom_toBottomOf="@+id/txtBotMessage" />

    </android.support.constraint.ConstraintLayout>

Now the modifications.

The name of the bot will be stored in the App class (com.pusher.pusherchat.App.kt), so add it next to the variable for the current user. The class should look like this:

    import android.app.Application

    class App:Application() {
        companion object {
            lateinit var user:String
            const val botUser = "bot"
        }
    }

Next, you need to modify the class com.pusher.pusherchat.MessageAdapter.kt to support the messages from the bot.

First, import the bot_message view and add a new constant for the bot’s messages outside the class:

    import kotlinx.android.synthetic.main.bot_message.view.*

    private const val VIEW_TYPE_MY_MESSAGE = 1
    private const val VIEW_TYPE_OTHER_MESSAGE = 2
    private const val VIEW_TYPE_BOT_MESSAGE = 3  // line to add

    class MessageAdapter (val context: Context) : RecyclerView.Adapter<MessageViewHolder>() {
        // ...
    }

Now modify the method getItemViewType to return this constant if the message comes from the bot:

    override fun getItemViewType(position: Int): Int {
        val message = messages.get(position)

        return if(App.user == message.user) {
            VIEW_TYPE_MY_MESSAGE
        } else if(App.botUser == message.user) {
            VIEW_TYPE_BOT_MESSAGE
        }
        else {
            VIEW_TYPE_OTHER_MESSAGE
        }
    }

And the method onCreateViewHolder, to inflate the view for the bot’s messages using the appropriate layout:

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
        return if(viewType == VIEW_TYPE_MY_MESSAGE) {
            MyMessageViewHolder(
              LayoutInflater.from(context).inflate(R.layout.my_message, parent, false)
            )
        }  else if(viewType == VIEW_TYPE_BOT_MESSAGE) {
            BotMessageViewHolder(LayoutInflater.from(context).inflate(R.layout.bot_message, parent, false))
        } else {
            OtherMessageViewHolder(LayoutInflater.from(context).inflate(R.layout.other_message, parent, false))
        }
    }

Of course, you’ll need the inner class BotMessageViewHolder so add it at the bottom of the class, next to the other inner classes:

    class MessageAdapter (val context: Context) : RecyclerView.Adapter<MessageViewHolder>() {
        // ...
        inner class MyMessageViewHolder (view: View) : MessageViewHolder(view) {
            // ...
        }

        inner class OtherMessageViewHolder (view: View) : MessageViewHolder(view) {
            // ...
        }

        inner class BotMessageViewHolder (view: View) : MessageViewHolder(view) {
            private var messageText: TextView = view.txtBotMessage
            private var userText: TextView = view.txtBotUser
            private var timeText: TextView = view.txtBotMessageTime

            override fun bind(message: Message) {
                messageText.text = message.message
                userText.text = message.user
                timeText.text = DateUtils.fromMillisToTimeString(message.time)
            }
        }
    }

Now you just need to set your Pusher app cluster and key at the beginning of the class ChatActivity and that’ll be all the code for the app.

Setting up Dialogflow

Go to Dialogflow and sign in with your Google account.

Next, create a new agent with English as its primary language:

Dialogflow will create two intents by default:

Default fallback intent, which it is triggered if a user's input is not matched by any other intent. And Default welcome intent, which it is triggered by phrases like howdy or hi there.

Create another intent with the name Trivia by clicking on the CREATE INTENT button or the link Create the first one:

Then, click on the ADD TRAINING PHRASES link:

And add some training phrases, like:

  • Tell me something about three
  • Give me a trivia about 4

You’ll notice that when you add one of those phrases, Dialogflow recognizes the numbers three and 4 as numeric entities:

Now click on the Manage Parameters and Action link. A new entity parameter will be created for those numbers:

When a user posts a message similar to the training phrases, Dialogflow will extract the number to this parameter so we can call the Numbers API to get a trivia.

But what if the user doesn’t mention a number?

We can configure another training phrase like Tell me a trivia and make the number required by checking the corresponding checkbox in the Action and parameters table.

This will enable the Prompts column on this table so you can click on the Define prompts link and enter a message like About which number? to ask for this parameter to the user:

Finally, go to the bottom of the page and enable fulfillment for the intent with the option Enable webhook call for this intent:

And click on SAVE.

Dialogflow will call the webhook on the app server API to get the response for this intent.

The webhook will receive the number, call the Numbers API and return the trivia to Dialogflow.

Let’s implement this webhook and the endpoint to post the messages and publish them using Pusher.

Building the server-side API

Open the server API project from the starter project in an IDE like IntelliJ IDEA Community Edition or any other editor of your choice.

Let’s start by adding the custom repository and the dependencies we are going to need for this project at the end of the file build.gradle:

    repositories {
        ...
        maven { url "https://jitpack.io" }
    }

    dependencies {
      ...
      compile('com.github.jkcclemens:khttp:-SNAPSHOT')
      compile('com.google.cloud:google-cloud-dialogflow:0.59.0-alpha')
    }

Next, in the package src/main/kotlin/com/example/demo, modify the class MessageController.kt so it looks like this:

    package com.example.demo

    import com.google.cloud.dialogflow.v2.*
    import com.pusher.rest.Pusher
    import org.springframework.http.ResponseEntity
    import org.springframework.web.bind.annotation.*
    import java.util.*

    @RestController
    @RequestMapping("/message")
    class MessageController {
        private val pusher = Pusher("PUSHER_APP_ID", "PUSHER_APP_KEY", "PUSHER_APP_SECRET")
        private val botUser = "bot"
        private val dialogFlowProjectId = "DIALOG_FLOW_PROJECT_ID"
        private val pusherChatName = "chat"
        private val pusherEventName = "new_message"

        init {
            pusher.setCluster("PUSHER_APP_CLUSTER")
        }

        @PostMapping
        fun postMessage(@RequestBody message: Message) : ResponseEntity<Unit> {
            pusher.trigger(pusherChatName, pusherEventName, message)

            if (message.message.startsWith("@$botUser", true)) {
                val messageToBot = message.message.replace("@bot", "", true)

                val response = callDialogFlow(dialogFlowProjectId, message.user, messageToBot)

                val botMessage = Message(botUser, response, Calendar.getInstance().timeInMillis)
                pusher.trigger(pusherChatName, pusherEventName, botMessage)
            }

            return ResponseEntity.ok().build()
        }

        @Throws(Exception::class)
        fun callDialogFlow(projectId: String, sessionId: String,
                           message: String): String {
            // Instantiates a client
            SessionsClient.create().use { sessionsClient ->
                // Set the session name using the sessionId and projectID 
                val session = SessionName.of(projectId, sessionId)

                // Set the text and language code (en-US) for the query
                val textInput = TextInput.newBuilder().setText(message).setLanguageCode("en")

                // Build the query with the TextInput
                val queryInput = QueryInput.newBuilder().setText(textInput).build()

                // Performs the detect intent request
                val response = sessionsClient.detectIntent(session, queryInput)

                // Display the query result
                val queryResult = response.queryResult

                println("====================")
                System.out.format("Query Text: '%s'\n", queryResult.queryText)
                System.out.format("Detected Intent: %s (confidence: %f)\n",
                        queryResult.intent.displayName, queryResult.intentDetectionConfidence)
                System.out.format("Fulfillment Text: '%s'\n", queryResult.fulfillmentText)

                return queryResult.fulfillmentText
            }
        }
    }

MessageController.kt is a REST controller that defines a POST endpoint to publish the received message object to a Pusher channel (chat) and process the messages of the bot.

If a message is addressed to the bot, it will call Dialogflow to process the message and also publish its response to a Pusher channel.

Notice a few things:

  1. Pusher is configured when the class is initialized, just replace your app information.
  1. We are using the username as the session identifier so Dialogflow can keep track of the conversation with each user.
  1. About the Dialogflow project identifier, you can click on the spinner icon next to your agent’s name:

To enter to the Settings page of your agent and get the project identifier:

For the authentication part, go to your Google Cloud Platform console and choose the project created for your Dialogflow agent:

Next, go to APIs & Services then Credentials and create a new Service account key:

Then, select Dialogflow integrations under Service account, JSON under Key type, and create your private key. It will be downloaded automatically:

This file is your access to the API. You must not share it. Move it to a directory outside your project.

Now, for the webhook create the class src/main/kotlin/com/example/demo/WebhookController.kt with the following content:

    package com.example.demo

    import khttp.responses.Response
    import org.json.JSONObject
    import org.springframework.web.bind.annotation.PostMapping
    import org.springframework.web.bind.annotation.RequestBody
    import org.springframework.web.bind.annotation.RequestMapping
    import org.springframework.web.bind.annotation.RestController

    data class WebhookResponse(val fulfillmentText: String)

    @RestController
    @RequestMapping("/webhook")
    class WebhookController {

        @PostMapping
        fun postMessage(@RequestBody json: String) : WebhookResponse {
            val jsonObj = JSONObject(json)

            val num = jsonObj.getJSONObject("queryResult").getJSONObject("parameters").getInt("number")

            val response: Response = khttp.get("http://numbersapi.com/$num?json")
            val responseObj: JSONObject = response.jsonObject

            return WebhookResponse(responseObj["text"] as String)
        }
    }

This class will:

  • Receive the request from Dialogflow as a JSON string
  • Extract the number parameter from that request
  • Call the Numbers API to get a trivia for that number
  • Get the response in JSON format (with the trivia in the text field)
  • Build the response with the format expected by DialogFlow (with the response text in the fulfillmentText field).

Here you can see all the request and response fields for Dialogflow webhooks.

And that’s all the code we need.

Configuring the Dialogflow webhook

We are going to use ngrok to expose the server to the world so Dialogflow can access the webhook.

Download and unzip ngrok is some directory if you have done it already.

Next, open a terminal window in that directory and execute:

    ngrok http localhost:8080

This will create a secure tunnel to expose the port 8080 (the default port where the server is started) of localhost.

Also, you should get a screen like this one:

Copy the HTTPS forwarding URL, in my case, https://5a4f24b2.ngrok.io.

Now, in your Dialogflow console, click on the Fulfillment option, enable the Webhook option, add the URL you just copied from ngrok appending the path of the webhook endpoint (webhook), and save the changes (the button is at the bottom of the page):

If you are using the free version of ngrok, you must know the URL you get is temporary. You’ll have to update it in Dialogflow every time it changes (either between 7-8 hours or when you close and reopen ngrok).

Testing the app

Before running the API, define the environment variable GOOGLE_APPLICATION_CREDENTIALS and set as its value the location of the JSON file that contains the private key you created in the previous section. For example:

    export GOOGLE_APPLICATION_CREDENTIALS=/path/to/my/key.json

For example, in my case, since I’m using Windows and IntelliJ IDEA to run the server, I just configure this environment variable in the run configuration for the class ChatbotApiApplication, the main class of the application:

Next, execute the following Gradle command in the root directory of the Spring Boot application:

    gradlew bootRun

Or if you’re using an IDE, execute the class ChatbotApiApplication.

Then, in Android Studio, execute your application on one Android emulator if you only want to talk to the bot. If you want to test the chat with more users, execute the app on two or more emulators.

This is how the first screen should look like:

Enter a username and use @bot to send a message to the bot:

Notice that if you don’t specify a number, the bot will ask for one, as defined:

Conclusion

You have learned the basics of how to create a chat app with Kotlin and Pusher for Android, integrating a chatbot using Dialogflow.

From here, you can extend it in many ways:

  • Train the bot to recognize more phrases
  • Use Firebase Cloud Functions instead of a webhook to call to the Numbers API (you’ll need a Google Cloud account with billing information)
  • Implement other types of number trivia
  • Use presence channels to be aware of who is subscribed to the channel

Here you can find more samples for Dialogflow agents.

Remember that all of the source code for this application is available at GitHub.

Clone the project repository
  • Android
  • Kotlin
  • Social
  • Social Interactions
  • Channels

Products

  • Channels
  • Chatkit
  • Beams

© 2019 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.