Building a chatbot for Android with Kotlin and Dialogflow

Introduction

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:

chatbot-kotlin-demo-1

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:

chatbot-kotlin-api-diagram

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:

1<?xml version="1.0" encoding="utf-8"?>
2    
3    <shape xmlns:android="http://schemas.android.com/apk/res/android"
4        android:shape="rectangle">
5    
6        <solid android:color="#11de72"></solid>
7    
8        <corners  android:topLeftRadius="5dp" android:radius="40dp"></corners>
9    
10    </shape>

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

1<!-- res/layout/bot_message.xml -->
2    <?xml version="1.0" encoding="utf-8"?>
3    <android.support.constraint.ConstraintLayout
4        xmlns:android="http://schemas.android.com/apk/res/android"
5        xmlns:app="http://schemas.android.com/apk/res-auto"
6        android:layout_width="match_parent"
7        android:layout_height="wrap_content"
8        android:paddingTop="8dp">
9    
10        <TextView
11            android:id="@+id/txtBotUser"
12            android:text="Trivia Bot"
13            android:layout_width="wrap_content"
14            android:layout_height="wrap_content"
15            android:textSize="12sp"
16            android:textStyle="bold"
17            app:layout_constraintTop_toTopOf="parent"
18            android:layout_marginTop="5dp" />
19    
20        <TextView
21            android:id="@+id/txtBotMessage"
22            android:text="Hi, Bot's message"
23            android:background="@drawable/bot_message_bubble"
24            android:layout_width="wrap_content"
25            android:layout_height="wrap_content"
26            android:maxWidth="240dp"
27            android:padding="15dp"
28            android:elevation="5dp"
29            android:textColor="#ffffff"
30            android:layout_marginTop="4dp"
31            app:layout_constraintTop_toBottomOf="@+id/txtBotUser" />
32    
33        <TextView
34            android:id="@+id/txtBotMessageTime"
35            android:text="12:00 PM"
36            android:layout_width="wrap_content"
37            android:layout_height="wrap_content"
38            android:textSize="10sp"
39            android:textStyle="bold"
40            app:layout_constraintLeft_toRightOf="@+id/txtBotMessage"
41            android:layout_marginLeft="10dp"
42            app:layout_constraintBottom_toBottomOf="@+id/txtBotMessage" />
43    
44    </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:

1import android.app.Application
2    
3    class App:Application() {
4        companion object {
5            lateinit var user:String
6            const val botUser = "bot"
7        }
8    }

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:

1import kotlinx.android.synthetic.main.bot_message.view.*
2    
3    private const val VIEW_TYPE_MY_MESSAGE = 1
4    private const val VIEW_TYPE_OTHER_MESSAGE = 2
5    private const val VIEW_TYPE_BOT_MESSAGE = 3  // line to add
6    
7    class MessageAdapter (val context: Context) : RecyclerView.Adapter<MessageViewHolder>() {
8        // ...
9    }

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

1override fun getItemViewType(position: Int): Int {
2        val message = messages.get(position)
3    
4        return if(App.user == message.user) {
5            VIEW_TYPE_MY_MESSAGE
6        } else if(App.botUser == message.user) {
7            VIEW_TYPE_BOT_MESSAGE
8        }
9        else {
10            VIEW_TYPE_OTHER_MESSAGE
11        }
12    }

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

1override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
2        return if(viewType == VIEW_TYPE_MY_MESSAGE) {
3            MyMessageViewHolder(
4              LayoutInflater.from(context).inflate(R.layout.my_message, parent, false)
5            )
6        }  else if(viewType == VIEW_TYPE_BOT_MESSAGE) {
7            BotMessageViewHolder(LayoutInflater.from(context).inflate(R.layout.bot_message, parent, false))
8        } else {
9            OtherMessageViewHolder(LayoutInflater.from(context).inflate(R.layout.other_message, parent, false))
10        }
11    }

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

1class MessageAdapter (val context: Context) : RecyclerView.Adapter<MessageViewHolder>() {
2        // ...
3        inner class MyMessageViewHolder (view: View) : MessageViewHolder(view) {
4            // ...
5        }
6    
7        inner class OtherMessageViewHolder (view: View) : MessageViewHolder(view) {
8            // ...
9        }
10    
11        inner class BotMessageViewHolder (view: View) : MessageViewHolder(view) {
12            private var messageText: TextView = view.txtBotMessage
13            private var userText: TextView = view.txtBotUser
14            private var timeText: TextView = view.txtBotMessageTime
15    
16            override fun bind(message: Message) {
17                messageText.text = message.message
18                userText.text = message.user
19                timeText.text = DateUtils.fromMillisToTimeString(message.time)
20            }
21        }
22    }

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:

chatbot-kotlin-create-trivia-bot

Dialogflow will create two intents by default:

chatbot-kotlin-default-intents

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:

chatbot-kotlin-create-intent

Then, click on the ADD TRAINING PHRASES link:

chatbot-kotlin-add-training-phrases

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:

chatbot-kotlin-recognizes-numbers

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

chatbot-kotlin-manage-parameters

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:

chatbot-kotlin-about-which-number

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

chatbot-kotlin-enable-webhook

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:

1repositories {
2        ...
3        maven { url "https://jitpack.io" }
4    }
5    
6    dependencies {
7      ...
8      compile('com.github.jkcclemens:khttp:-SNAPSHOT')
9      compile('com.google.cloud:google-cloud-dialogflow:0.59.0-alpha')
10    }

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

1package com.example.demo
2    
3    import com.google.cloud.dialogflow.v2.*
4    import com.pusher.rest.Pusher
5    import org.springframework.http.ResponseEntity
6    import org.springframework.web.bind.annotation.*
7    import java.util.*
8    
9    @RestController
10    @RequestMapping("/message")
11    class MessageController {
12        private val pusher = Pusher("PUSHER_APP_ID", "PUSHER_APP_KEY", "PUSHER_APP_SECRET")
13        private val botUser = "bot"
14        private val dialogFlowProjectId = "DIALOG_FLOW_PROJECT_ID"
15        private val pusherChatName = "chat"
16        private val pusherEventName = "new_message"
17    
18        init {
19            pusher.setCluster("PUSHER_APP_CLUSTER")
20        }
21    
22        @PostMapping
23        fun postMessage(@RequestBody message: Message) : ResponseEntity<Unit> {
24            pusher.trigger(pusherChatName, pusherEventName, message)
25    
26            if (message.message.startsWith("@$botUser", true)) {
27                val messageToBot = message.message.replace("@bot", "", true)
28    
29                val response = callDialogFlow(dialogFlowProjectId, message.user, messageToBot)
30    
31                val botMessage = Message(botUser, response, Calendar.getInstance().timeInMillis)
32                pusher.trigger(pusherChatName, pusherEventName, botMessage)
33            }
34    
35            return ResponseEntity.ok().build()
36        }
37    
38        @Throws(Exception::class)
39        fun callDialogFlow(projectId: String, sessionId: String,
40                           message: String): String {
41            // Instantiates a client
42            SessionsClient.create().use { sessionsClient ->
43                // Set the session name using the sessionId and projectID 
44                val session = SessionName.of(projectId, sessionId)
45    
46                // Set the text and language code (en-US) for the query
47                val textInput = TextInput.newBuilder().setText(message).setLanguageCode("en")
48    
49                // Build the query with the TextInput
50                val queryInput = QueryInput.newBuilder().setText(textInput).build()
51    
52                // Performs the detect intent request
53                val response = sessionsClient.detectIntent(session, queryInput)
54    
55                // Display the query result
56                val queryResult = response.queryResult
57    
58                println("====================")
59                System.out.format("Query Text: '%s'\n", queryResult.queryText)
60                System.out.format("Detected Intent: %s (confidence: %f)\n",
61                        queryResult.intent.displayName, queryResult.intentDetectionConfidence)
62                System.out.format("Fulfillment Text: '%s'\n", queryResult.fulfillmentText)
63    
64                return queryResult.fulfillmentText
65            }
66        }
67    }

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:

  • Pusher is configured when the class is initialized, just replace your app information.

  • We are using the username as the session identifier so Dialogflow can keep track of the conversation with each user.

  • About the Dialogflow project identifier, you can click on the spinner icon next to your agent’s name:

chatbot-kotlin-dialogflow-identifier-1

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

chatbot-kotlin-dialogflow-identifier-2

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

chatbot-kotlin-google-console

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

chatbot-kotlin-create-key-1

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

chatbot-kotlin-create-key-2

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:

1package com.example.demo
2    
3    import khttp.responses.Response
4    import org.json.JSONObject
5    import org.springframework.web.bind.annotation.PostMapping
6    import org.springframework.web.bind.annotation.RequestBody
7    import org.springframework.web.bind.annotation.RequestMapping
8    import org.springframework.web.bind.annotation.RestController
9    
10    data class WebhookResponse(val fulfillmentText: String)
11    
12    @RestController
13    @RequestMapping("/webhook")
14    class WebhookController {
15    
16        @PostMapping
17        fun postMessage(@RequestBody json: String) : WebhookResponse {
18            val jsonObj = JSONObject(json)
19    
20            val num = jsonObj.getJSONObject("queryResult").getJSONObject("parameters").getInt("number")
21    
22            val response: Response = khttp.get("http://numbersapi.com/$num?json")
23            val responseObj: JSONObject = response.jsonObject
24    
25            return WebhookResponse(responseObj["text"] as String)
26        }
27    }

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.

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):

chatbot-kotlin-add-webhook

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

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:

chatbot-kotlin-login

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

chatbot-kotlin-demo-1

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

chatbot-kotlin-demo-2

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.