Adding Chatbots to Your Stream Chat App Using Google’s Dialogflow
Originally published at https://getstream.io.
Most tasks are repetitive and time-consuming. What if we could use a bot to help in doing the tasks instead? This will hugely increase productivity. Chatbots are fantastic for this.
In this tutorial, I’ll walk you through steps to adding a chatbot to your Stream Chat app using Dialogflow and Vue. At the end of this tutorial, you will build a frequently asked questions (FAQs) bot, be more familiar with Stream Chat API, and the webhook system provided by Stream.
Here is a preview of what we’ll be building:
Prerequisites
To follow along with this tutorial comfortably, the following are expected:
- A basic understanding of JavaScript
- A basic understanding of Vue.js
- You should also have Node installed on your system (version 8.9 or above – preferably the latest 12.x)
- You should have npm or yarn installed on your system
- Have ngrok installed on your system
If you have all the above ready, then let’s get started!
Set up the Client App – Create a New Vue App
Vue provides a CLI for scaffolding a new Vue project. First, you'll need to install the Vue CLI globally on your system (if you don't have it installed already). After that, we’ll create a new Vue project with CLI commands.
Next, create a new Vue project by running the following commands in any convenient location on your system:
# Install Vue CLI globally on your system
$ yarn global add @vue/cli
# Create a new Vue project (for the prompt that appears, press enter to select the default preset.)
$ vue create faqx
# Change your directory to the project directory
$ cd faqx
# Install Stream Chat client SDK
$ yarn add stream-chat
# Install axios - a library for making request
$ yarn add axios
# Run the app!
$ yarn serve
Accessing the URL displayed on your terminal will take you to a Vue default page.
Get Your Stream Chat Keys
To start using the Stream Chat API, you need to have an API key.
Create an account here or log in if you already have an account.
Then, from your dashboard:
- Create a new app using any convenient name. I will call mine “FAQ”
- Fill in the App Name as “FAQx" or any name you wish
- Finally, submit the form by clicking on the Submit button to create the app
Once the app is created, you will be provided with some options on your dashboard. Select Chat to go the Stream Chat dashboard.
Next, check Disable Permissions Checks and then click on the Save button.
Then, scroll to the bottom of the page and take note of your App Access Keys – Key and Secret.
Next, create a .env
file in the root directory of the project and update your Stream keys to it:
VUE_APP_PORT=3000
VUE_APP_SERVER=http://localhost:3000
VUE_APP_KEY=<YOUR KEY>
APP_SECRET=<YOUR SECRET>
DIALOGFLOW_PROJECT_ID=<YOUR DIALOGFLOW PROJECT ID>
GOOGLE_APPLICATION_CREDENTIALS=<GOOGLE SERVICE FILE>
Remember to replace <YOUR KEY>
and <YOUR SECRET>
with the correct app keys you have noted above.
Set up the Server
Next, create a new directory named server
in the root directory of the project. Then open up a new terminal and then change your current directory to the server
directory. Then install the following dependencies that we’ll need for the server:
$ yarn add express cors body-parser dialogflow path uuid dotenv
The above dependencies include:
-
express
– A Node framework we are using to build our server -
dialogflow
– Dialogflow’s Node SDK -
dotenv
– An npm package for parsing configs from.env
files - cors, body-parser, uuid and dotenv (all excellent npm packages)
Next, create an Express app by creating a new file named app.js
to the server
directory and then add the following code to it:
// ./server/app.js
const express = require("express");
const cors = require("cors");
const bodyParser = require("body-parser");
const StreamChat = require('stream-chat').StreamChat;
const dialogflow = require('dialogflow').v2beta1;
const uuid = require('uuid');
require('dotenv').config({path: "../.env"})
const app = express();
const port = process.env.VUE_APP_PORT || 3000
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Initialize Stream Chat SDK
const serverSideClient = new StreamChat(
process.env.VUE_APP_KEY,
process.env.APP_SECRET
);
app.get("/", async (req, res) => {
res.send({ hello: "World!" });
});
app.listen(port, () => {
console.log(`Node app listening on port ${port}!`)
});
In the code above, after importing the packages that we have installed,
- We created an Express app using
const app = express();
- Next, we initialize the Stream Chat SDK
- Then finally, we created an endpoint -
/
(app.get('/',…
) for testing to know if the app works. So, if you visithttp://localhost:3000/
, you will get a message -{ hello: 'World!'}
Next, create a new endpoint for creating a token.
// ./server/app.js
app.post('/login', async (req, res) => {
const userId = req.body.userId
if (!userId) {
return res.status(400).send({
status: "error",
message: "username and name is required"
})
}
return res.status(200).send({
status: "success",
token: serverSideClient.createToken(userId)
})
});
The token is required for initializing the Stream Chat SDK.
Finally, start up the server:
node app.js
If it starts successfully, you will see a message printed to the terminal - “Node app listening on port 3000!”
Set up Dialogflow
Dialogflow is a Google-owned developer of human-computer interaction technologies based on natural language conversations. Before you can start using the service of Dialogflow, you need to have an account with them.
Now head to Dialogflow’s website and create a free account if you don’t have one already. You’ll need a google account to be able to create an account. Once you are on the page, click on the sign in with Google button.
From your dashboard,
- Create an agent by selecting Create Agent.
- Type in the Agent name as FAQX.
- Click the Create button to processes the form.
Next,
- Click on the gear icon, to the right of the agent name.
- Enable beta features and APIs.
- Copy your project ID and update the .env file with it: DIALOGFLOW_PROJECT_ID=
- Click the Save button to save your changes
- Under the GOOGLE PROJECT section, click on the name of the Service Account.
This will take you to the Google Cloud Platform Service Accounts page, but you first need to update the Service Account's role.
Once the page finishes loading:
- Click on the menu button in the upper left-hand corner and click on IAM & admin.
- Click on Service Accounts in the left-hand menu.
- Click on the Create Service Account button at the top of the page.
- In the next page, enter a name for the service account and then click on the CREATE button to submit the form.
- Click on Role and select Owner as the role.
Then click on the CONTINUE Button.
- Click on the + CREATE KEY button. Click the CREATE to download your service key.
A download of the JSON file will start. Copy the download file to the server
directory of the project.
Then update the .env
file to reflect the name of the JSON file you just downloaded:
GOOGLE_APPLICATION_CREDENTIALS=<GOOGLE SERVICE FILE>
Prepare Your FAQ Data
Here is where we prepare our frequently asked questions. Create a new file in any location on your system as faq.csv
. Then, copy the below FAQ samples to it and save.
How can I place orders?, "There are two ways to place orders for our services:"
What does an order cost?, "The costs are calculated according to the amount of time involved and the quality requirements. These vary from project to project. Therefore, per project, you will receive an individual offer on demand."
The above is just an example for our testing. Feel free to replace with yours. The first column is the question, while the second column is the answer. The data are comma-separated and no empty rows are expected, else, you will get an error.
Here is a simple example of the format:
An example question 1, An answer 1
An example auestion 2, An answer 2
From the data on the faq.csv
file, when a user sends in a query as - “How can I place orders?”. The expected output from Dialogflow will be - “here are two ways to place orders for our services:” and so on.
Next, go back to your Dialogflow dashboard,
- Click on the Knowledge tab on the left the page
- Click on the CREATE KNOWLEDGE BASE button
- Type in FAQ as the name of the knowledge
- Click on the Create the first one link
On the popup that appears,
- Enter the Document Name as “FAQ”
- Choose “FAQ" as the knowledge type
- Choose
txt/csv
as the Mime Type - Choose Upload file from your computer as the DATA SOURCE
- Click on the SELECT FILE button and browse through to the CSV file you created above and upload it
- Then, click the CREATE button
- Click on ADD RESPONSE
- Finally, click the SAVE button to register your changes
Building the App Interface
Now that we have both our client and server apps running, the next thing we’ll do is to create a simple chat interface so we can integrate Dialogflow.
Vue enables us to build reusable components which make up our app user interface. We’ll split the app UI into smaller components so we can build them separately:
For brevity’s sake, we’ll divide the app into two components:
-
Messages.vue
*- *for listing messages and a form input -
Login.vue
- displays the login form
Create the Messages.vue
and Login.vue
files inside the src/components
directory.
To keep the component file as minimal as possible, I have added all the CSS of the components to a single CSS file. Create the CSS file as App.css
in the src
directory of the project and then add the below styles to it:
/* ./App.css */
html,body{
width: 100%;
height: 99vh;
overflow: hidden;
}
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
.login-container {
display: grid;
grid-template-rows: 4fr 1fr;
max-height: 320px;
min-height: 320px;
}
input[type="text"] {
padding: 10px 8px;
margin-top: 10px;
border-radius: 2px;
border: 1px solid darkgray;
font-size: 16px;
box-sizing: border-box;
display: block;
}
.inputs {
text-align: center;
align-self: center;
justify-self: center;
}
.submit {
margin-top: 9px;
padding: 20px;
background: rgb(99, 99, 212);
color: white;
font-size: 16px;
}
.chat-message {
width: 70%;
margin-top: 4px;
padding: 4px;
text-align: left;
word-wrap: break-word;
margin: 5px;
}
.from-admin {
background: rgb(150, 178, 183);
color: rgb(39, 37, 37);
float: left;
}
.from-client {
background: rgb(48, 13, 79);
color: white;
float: right;
}
.input-container {
margin: 0px;
}
.chat-input {
width: 99%;
margin-bottom: 0px;
}
.client-messages {
overflow-y: scroll;
height: 320px;
}
.chat-container {
position: fixed;
right: 0px;
bottom: 0px;
width: 400px;
z-index: 100000;
box-sizing: border-box;
}
.head {
padding: 9px;
display: grid;
background-color: rgb(48, 13, 79);
color: white;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
.chat-box {
border-left: 1px solid rgb(48, 13, 79);
border-right: 1px solid rgb(48, 13, 79);
background: lightgray;
}
The Login Component
We’ll be using the Single File Component structure for our components.
Add the mark-up for the Login component:
<!-- ./src/components/Login.vue -->
<template>
<div class="login-container chat-box">
<div class="inputs">
<input type="text" placeholder="your username" v-model="username">
</div>
<button type="submit" class="submit" v-on:click="login"> Submit </button>
</div>
</template>
In the preceding code, we added an input form that captures the user username. Also, we added a click event that calls the login
function when the form is submitted.
Next, add the script section for the Login component.
<!-- ./src/components/Login.vue -->
<script>
export default {
data: function () {
return {
username: "",
}
},
methods: {
login() {
this.$emit("authenticated", this.username);
}
}
}
</script>
Here, we defined the login function that will be called on the mark-up file. In the function, we emit a custom event named authenticated
to the parent component passing the username along.
The Messages Component
Define the messages component that lists messages and holds the form input for adding new messages:
<!-- ./src/components/Messages.vue -->
<template>
<div class="chat-box">
<div class="client-messages ">
<div
class="chat-message"
v-for="message in messages"
v-bind:key="message.id"
v-bind:class="[(message.user.id == userId) ? 'from-client' : 'from-admin']"
>
{{ message.text }}
</div>
</div>
<div class="input-container">
<input
class="chat-input"
type="text"
placeholder="enter message..."
v-model="message"
v-on:keyup.enter="addMessage"
>
</div>
</div>
</template>
Then add the script section for the messages component:
<!-- ./src/components/Messages.vue -->
<script>
export default {
props: ['messages', 'userId'],
data() {
return {
message: ""
}
},
methods: {
addMessage() {
this.$emit('new-message', this.message);
this.message = "";
}
}
}
</script>
The App Component
Now let’s bring all the other component into a single component to build up the interface. Replace the code in the src/App.vue
file with the below mark-up:
<!-- ./src/App.vue -->
<template>
<div id="app">
<div>
<img alt="Vue logo" src="./assets/logo.png">
</div>
<div class="chat-container">
<div class="head" @click="displayChatArea=!displayChatArea">
<div style="justify-self: center;"> FAQx </div>
</div>
<div v-if="displayChatArea">
<login
v-if="!authenticated"
v-on:authenticated="setAuthenticated"
/>
<messages
v-else
v-on:new-message="sendMessage"
:messages="messages"
:userId="userId"
/>
</div>
</div>
</div>
</template>
Then add the script section to the src/App.vue
file:
<!-- ./src/App.vue -->
<script>
import { StreamChat } from 'stream-chat';
import axios from 'axios';
import Messages from '@/components/Messages.vue';
import Login from '@/components/Login.vue';
import './App.css';
export default {
name: 'app',
components: {
Messages,
Login
},
data: function () {
return {
authenticated: false,
roomId: "",
messages: [],
userId: "",
displayChatArea: false,
channel: null,
token: null,
client: null
}
},
methods: {
},
}
</script>
The Vue app should look like below at this point:
Making Chat Work
So far, we have our chat interface ready but we still can’t converse because we are yet to connect the app to Stream Chat. We’ll be doing so next.
To start using the SDK, we first need to initialize it. Do so by adding the below function in the methods: {…}
block in the App.vue
file:
// ./src/App.vue
// [...]
async initializeClient () {
// Initialize the StreamChat SDK
const client = new StreamChat(process.env.VUE_APP_KEY);
await client.setUser(
{
id: this.userId,
name: this.userId,
},
this.token,
);
this.client = client
},
// [...]
Next, add a function for creating a new channel for the user that just logs in to the methods: {…}
block of App.vue
:
// ./src/App.vue
// [...]
async initializeChannel () {
const channel = this.client.channel('commerce', this.userId, {
name: 'Customer Support',
});
this.channel = channel
// fetch the channel state, subscribe to future updates
let state = await channel.watch();
this.messages = state.messages
// Listen for new messages
channel.on('message.new', event => {
this.messages.push(event.message)
});
},
// [...]
Here, we are creating a new channel for every user that logs in so that their messages are sperate from other users. Then we started listening for new messages on that channel using - channel.on('message.new...'
.
Next, add a function for logging the user into the methods: {…}
block of App.vue
:
// ./src/App.vue
// [...]
async setAuthenticated(userId) {
const response = await axios.post(`${process.env.VUE_APP_SERVER}/login`, {userId})
if (response.data.status === 'success') {
this.authenticated = true
this.token = response.data.token
this.userId = userId
await this.initializeClient()
await this.initializeChannel()
}
},
// [...]
In the proceeding code, we make a post request to the /login
endpoint on the server to generate a token for the user. The token is required for initializing the StreamChat SDK.
Finally, add a function for sending messages into the methods: {…}
block of App.vue
:
// ./src/App.vue
// [...]
async sendMessage (message) {
this.channel && await this.channel.sendMessage({
text: message
});
},
// [...]
When the user submits the message form, we’ll call this function to send a message to the channel.
Set up the Webhook
For now, we can send a message from to channels but we don’t get a reply because there is no one on the other end to reply. We’ll now employ the service of a chatbot to reply to those messages.
Next, create a function to query Dialogflow for an answer to a given input:
// ./server/app.js
// [...]
async function getAnswer (question) {
const sessionId = uuid.v4();
// Create a new session
const sessionClient = new dialogflow.SessionsClient();
const sessionPath = sessionClient.sessionPath(
process.env.DIALOGFLOW_PROJECT_ID,
sessionId
);
// The text query request.
const request = {
session: sessionPath,
queryInput: {
text: {
// The query to send to the dialogflow agent
text: question,
// The language used by the client (en-US)
languageCode: 'en-US',
},
},
};
// Send request and log result
const responses = await sessionClient.detectIntent(request);
return responses[0].queryResult.fulfillmentText;
}
// [...]
By using webhooks, you can receive all events within your application. When configured, every event happening on Stream Chat will propagate to your webhook endpoint via an HTTP POST request. For our use-case, we are most interested in the "message.new"
event.
Next, create an endpoint for our webhook. Stream will make a request to this endpoint for every new message sent to the channel.
Add the below code to the server/app.js
file:
// ./server/app.js
// [...]
app.post('/webhook', async (req, res) => {
// extract data from the POST payload
const { cid, type, message, user } = req.body
// Get the organization and channel from the cid param
const [org, chan] = cid.split(':')
res.status(200).send({
status: "success"
})
// Make sure the new message is not comming form the bot
// We are using 'bandit' as a username for our bot
if (type === "message.new" && user.id !== 'bendit') {
const channel = serverSideClient.channel(org, chan);
const messagePayload = {
text: await getAnswer(message.text),
user: {id: 'bendit'},
}
try {
await channel.sendMessage(messagePayload);
} catch(e) {
console.log(e.message)
}
}
});
// [...]
In the proceeding code:
- After extracting the data from the POST request, we check the type of the event if it’s a "message.new" event. Also, we want to make sure that the message is not coming from the bot - 'bendit'. ‘bandit’ is just a generic username we want to use as our bot. It can be any name you like.
- Next, we call the getAnswer function to query Dialogflow for an answer to the new message that just came in.
- Then finally, we call the
channel.sendMessage(…
function to send the result from Dialogflow to the channel so that the user can see the reply.
Enable the Webhook
Although we now have our webhook ready which can be accessed from http://localhost:3000/webhook. It is only accessible from your local system. We need to expose the URL so it can be accessible by Stream which we can do using ngrok.
Open up ngrok and expose the URL:
./ngrok http 3000
Now note any of the Forwarding URLs which can now be accessed from anywhere.
Head to your Stream Dashboard and enable the webhook option:
- Scroll down to the Chat Event section
- Enable the webhook by selecting the Active selection
- Enter the ngrok generated URL in the URL input form
- Click the Save button to save your changes
Testing Chat
Good job! you have successfully built a FAQ bot. Now let’s test the app to see that it works:
- Restart the Node server terminal
- Open the Vue app on your browser
- Now, ask a user for a given question and see the response from the bot
Final Thoughts
In this tutorial, you have successfully built a FAQ bot . We explored a way in which you can add bots to your Stream app using the webhook feature provided by the Stream Chat infrastructure.
The sky is the limit for what you can build from here. What we've built is just a tip of the iceberg in terms of what you can do with bots. You can extend the functionality of the bot by adding more intents with Dialogflow.
You can find the complete code of this tutorial on GitHub. For more information on Stream Chat, have a look here.
Top comments (0)