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.
Here’s what you need to have installed/configured to follow this tutorial:
I also assume that you are familiar with:
Let’s get started.
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.
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.
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:
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.
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:
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:
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:
number
parameter from that requesttext
field)fulfillmentText
field).Here you can see all the request and response fields for Dialogflow webhooks.
And that’s all the code we need.
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).
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:
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:
Here you can find more samples for Dialogflow agents.
Remember that all of the source code for this application is available at GitHub.