Building a chatbot for Android with Kotlin and Dialogflow
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')
}
- khttp, an HTTP library to make requests to the Numbers API.
- Google Cloud Java Client for Dialogflow, to call Dialogflow’s API.
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:
-
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:
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.
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
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.
19 September 2018
by Esteban Herrera