The Stream Blog

Adding Chatbots to Your Stream Chat App Using Google’s Dialogflow

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:

$ 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 visit http://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!”

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:

  1. Click on the gear icon, to the right of the agent name.
  2. Enable beta features and APIs.
  3. Copy your project ID and update the .env file with it:

DIALOGFLOW_PROJECT_ID=<YOUR DIALOGFLOW PROJECT ID>

  1. Click the Save button to save your changes
  2. 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:

  1. Click on the menu button in the upper left-hand corner and click on IAM & admin.
  2. Click on Service Accounts in the left-hand menu.
  3. Click on the Create Service Account button at the top of the page.
  4. In the next page, enter a name for the service account and then click on the CREATE button to submit the form.
  5. 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:"
Which method of ordering is best for me?, "In the areas of standardized text creation (with and without keywords), translations and survey tasks, it is more advantageous and favorable for you to enter the order using the Self-Service Marketplace. If you have large, individual projects from any service areas offered, please contact our sales team. They will provide personal advice and discuss the project with you."
What preliminary information is needed for an offer?, "Depending on the service requested we will need the following specifications:"
How great is the risk that the results do not meet my expectations?, "We discuss the outcome requirements as accurately as possible in advance. Furthermore, when we are dealing with large orders, we agree on a test order to ensure correct and faultless processing."
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 is 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.

  1. Click on the Knowledge tab on the left the page
  2. Click on the CREATE KNOWLEDGE BASE button
  3. Type in FAQ as the name of the knowledge
  4. Click on the Create the first one link

On the popup that appears, 

  1. Enter the Document Name as “FAQ” 
  2. Choose “FAQ" as the knowledge type
  3. Choose txt/csv as the Mime Type 
  4. Choose Upload file from your computer as the DATA SOURCE
  5. Click on the SELECT FILE button and browse through to the CSV file you created above and upload it
  6. Then, click the CREATE button
  7. Click on ADD RESPONSE
  8. 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
    },
// [...]

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 or using some of the chat messaging ui kit patterns we've developed.

You can find the complete code of this tutorial on GitHub. For more information on Stream Chat, have a look here.