ChatBot and Chat Support in React Native

How to integrate ChatBots and live support into your app for a better customer experience

ChatBot and Chat Support in React Native

The era of AI and human integration has begun and a major percentage of firms have started utilising this technique to increase their business. The ease of communication, in regards to any product or service, is a pro that customers look for and AI fulfils this need. The majority of websites/applications have ChatBots to serve the customer's queries and live support for efficient customer engagement. In a nutshell, AI is now a vital part of determining the success of a customer's acquisition, retention, and sales.

This article covers the topic of integration of Live Chat Support via Slack and Google DialogFlow to React Native.

This article assumes that the reader has basic working knowledge of React Native and Node JS along with basic knowledge of Socket IO, Google APIs, and Slack APIs.

Integrating Google Dialogflow

Dialogflow is a natural language understanding platform that makes it easy to design and integrate a conversational user interface into your mobile app, web application, device, bot, interactive voice response system, and so on. The following are the steps to integrate Google's Dialog Flow into our project:

  • Create a project in Google Console and enable the DialogFlow API.
  • Next, generate JSON key for the DialogFlow API (Service Account).

1.png

  • After this, go to the DialogFlow Console and create an agent and map it to the created project.
  • Next, create an intent to map the customers' queries with the responses.

    For saving time, we can also enable the Small Talk feature to utilize the built-in introductory responses.

2.png

Installation of Google DialogFlow

In this article, we use Expo for RN development and a well-known package, React-Native-Gifted-Chat, for the chat component of the RN app.

  • Install the dependencies while making sure the [socket.io](http://socket.io)-client version and socket.io in API are compatible. As of Socket.IO 3.1.0, the v3 server can now communicate with v2 clients. The result, at this point, should look like:
"dependencies": {
    "@types/socket.io-client": "^1.4.33",
  "expo": "^40.0.0",
    "socket.io-client": "^2.3.0",
}

Implementation of Google DialogFlow:

  • Firstly, import the JSON key which we acquired from Google console to initialise DialogFlow like this:

      initializeDialogFlow = () => {
          this.dialogflow.setConfiguration(
            dialogFlowConfig.client_email,
            dialogFlowConfig.private_key,
            'en-US',
            dialogFlowConfig.project_id,
          );
        };
    
  • Initializing DialogFlow would require generating AccessToken by making an API call to: https://accounts.google.com/o/oauth2/token

  • Once we get the AccessToken we can make an API request to DialogFlow using the endpoint https://dialogflow.googleapis.com/v2beta1/projects/ like this:

      requestQuery = async (query: string, onResult: Function, onError: Function) => {
          const data = {
            queryInput: {
              text: {
                languageCode: this.languageTag,
                text: query,
              },
            },
          };
          const url = `${DEFAULT_BASE_URL}${this.projectId}/agent/sessions/${this.sessionId}:detectIntent`;
    
          fetch(url, {
            body: JSON.stringify(data),
            headers: {
              Authorization: `Bearer ${this.accessToken}`,
              'Content-Type': 'application/json; charset=utf-8',
              charset: 'utf-8',
            },
            method: 'POST',
          })
            .then((response) => {
              return response.json().then(onResult);
            })
            .catch(onError);
        };
    

Next, let's get around to creating the Chat Component, as shown below:

import { Composer, GiftedChat, Send } from 'react-native-gifted-chat';
<View style={styles.basicContainer}>
    <GiftedChat
      alwaysShowSend
      bottomOffset={ANDROID ? -10 : BOTTOM_TABBAR_HEIGHT}
      isTyping={isTyping}
      messages={messages}
      onSend={(messages) => this.onSend(messages)}
      parsePatterns={this.getParsePatterns}
      renderAvatar={this._renderAvatar}
      renderUsernameOnMessage
      user={{
        _id: 1,
        avatar: USER.gravatar,
        name: USER.name,
      }}
    />
  {ANDROID && <KeyboardAvoidingView />}
</View>

component.png

Gifted-Chat provides multiple customisation options along with their components.

  • When the user enters a message, it is sent to Google Dialogflow where an automated reply is created like this:
// Request to Google dialog flow
  sendToGoogle = async (message) => {
    const text = message[0].text;
    this.dialogflow.requestQuery(
      text,
      (result) => this.handleGoogleResponse(result),
      (error) => this.appendBotResponse(CONNECTION_ERROR_MESSAGE),
    );
  };
  • Apart from just sending a text response, DialogFlow can also send payload objects along with the response text. This can be used for checking and other purposes.

DialogFlow.png

  • Based on the payload, we can initiate a human-takeover. In this example, we send connectSlack: true as a custom payload to pause the ChatBot. Then, we have to initiate a socket connection to interact with support team members. This is further explained below

      handleGoogleResponse = (response) => {
          const { queryResult } = response;
          if (!queryResult) {
              this.appendBotResponse(CONNECTION_ERROR_MESSAGE);
          }
          const { fulfillmentMessages } = queryResult;
          if (fulfillmentMessages) {
              const text = fulfillmentMessages[0].text.text[0];
            // If we have payload from DialogFlow
          if (fulfillmentMessages[1] && fulfillmentMessages[1].payload) {
              const payload = fulfillmentMessages[1].payload;
                      if (payload.connectSlack) {
                          // Create a socket connection and send the message
                          const socketOptions = {
                        query: { user: JSON.stringify(this.props.user) },
                        secure: true,
                      };
                          this.socket = SocketIo(config.socket.url, socketOptions);
                          // Wait for socket connection to be created before creating channel
                    setTimeout(() => {
                      this.socket && this.socket.emit('createChannel', { user, message });
                      }, 300);
                      }
              }
          } else {
              // No pre-defined responses
          const text = `Sorry, I couldn't understand that.`;
          }
      }
    

To set up Slack

Slack is like a chatroom for your whole team. It is a computer application that was created as a way for organizations to communicate both as a group and in personal one-on-one discussions. You can communicate as a group through channels or privately via direct messaging. Steps to integrate Slack with Node in our project:

  • First, create a basic Node Js app with a single end-point.
  • Next, create an account in Slack and create a new App.

5.png

  • Get the Client ID and Client Secret and Signing Secret from the Basic Information page.
  • With this data, we can create OAuth credentials and give required scope permissions from OAuth & Permissions tab. We should end up with a result as the picture shown below, at this point:

tokens.png

  • In the next step, we need to add scopes to the bot. For example, your bot must be able to create a channel so we give channels:manage scope. After following this step, the bot will be enabled to manage public channels, as well enable new ones. Similarly, we also have to issue commands like channels:join channels:read chat:write etc. Refer to the image given below to understand this concept better:

scopes.png

Use ngrok and create tunnels to provide an endpoint to a Slack Event. This is so that Slack can trigger an event when someone is interacting through the chat medium.

events.png

Installation of Slack

To begin, create a basic Express app and install the dependencies shown below:

"dependencies": {
    "@slack/events-api": "^2.3.3",
    "@slack/web-api": "^5.10.0",
    "express": "^4.16.4",
    "socket.io": "^2.3.0",
  "socket.io-redis": "^5.3.0",
}

Implementation of Slack and Socket IO:

We have to create a Slack Channel for every new user. Specified slack members are added to the channel at the time of channel creation with their Slack ID. When a user initiates a socket connection from a client, we can also check information about the event using handle based on the same.

  • Initiate Socket IO as follows:

      // index.ts
      import express from 'express';
      import { createServer } from 'http';
    
      const app = express();
      export const server: any = createServer(app);
      export const io = require('socket.io').listen(server);
    
  • Create an endpoint for handling Slack events. We need to add a Slack Event Endpoint Middleware before the body-parser. At this point, the code should look like:

      // index.ts
      import SlackSocketClient from './src/util/SlackSocketClient';
      import express from 'express';
      import { createServer } from 'http';
    
      const app = express();
      app.use(cors(corsOptions));
      export const server: any = createServer(app);
      // Slack event handler - this statement should be before bodyParser
      // Because the adapter can only work if it comes before the body parser in the middleware stack
      app.post('/slack/events', SlackSocketClient.slackEvents.expressMiddleware());
      app.use(bodyParser.json({ limit: '10mb' }));
    
  • Next, we have to initialise the Slack Client using the following tokens:

      // SlackSocketClient/index.ts
      import { WebClient } from '@slack/web-api';
      import { createEventAdapter } from '@slack/events-api';
      const SLACK_BOT_TOKEN = (process as any).env.SLACK_BOT_TOKEN;
      const SLACK_BOT_SIGNING_SECRET = (process as any).env.SLACK_BOT_SIGNING_SECRET;
    
      this.slackEvents = createEventAdapter(SLACK_BOT_SIGNING_SECRET);
      this.slackWeb = new WebClient(SLACK_BOT_TOKEN);
    
  • After this, define the Socket connection and create callbacks for the respective events. This step is better understood with the usage of the following code:

      // index.ts
      // Socket connections
      // This is used for opening a socket connection with the client for our chatbot
      io.on('connection', async (socket: any) => {
      // When we receive a message from the client
      socket.on('clientMessage', (data: any) => {
      // Send to Slack
      SlackSocketClient.sendToSlack(data);
      });
      // Create a Slack channel
      socket.on('createChannel', (data: any) => {
        SlackSocketClient.joinOrCreateChannel(data, socket);
      });
      // Log socket errors
      socket.on('error', (err: any) => {
        Logger.log(err, 'error');
      });
      socket.on('disconnect', () => {
          // Disconnect the socket
          socket.disconnect(true);
        });
      })
    
  • When a new user is initiating a socket connection and sending createChannel event, the first step is to check is there is an existing channel for the user. If so we join the same channel or else we have to create a new channel. For this example, we shall create the Slack channel name in a standard format such as dev-{firstName}{lastName}-{user.id}dev-JamesBond-1. This is as shown below:

      // SlackSocketClient/index.ts
      async createChannel(data: any, socket: any, messageToSend: string) {
          const channelToUse = formatChannelName(data.user);
          // Create a channel on Slack for the new user
          const response: any = await this.slackWeb.conversations.create({
            token: SLACK_BOT_TOKEN,
            name: channelToUse,
            is_private: false,
            user_ids: SLACK_TEAM_MEMBER_IDS,
          });
          // Set channel topic as client's email for reference
        await this.slackWeb.conversations.setTopic({
          token: SLACK_BOT_TOKEN,
          channel: response.channel.id,
          topic: data.user.email,
        });
        // Invite team members to the channel on Slack
        await this.slackWeb.conversations.invite({
          token: SLACK_BOT_TOKEN,
          channel: response.channel.id,
          users: SLACK_TEAM_MEMBER_IDS,
        });
      }
    
  • Once the channel is created, we use it as an identifier while making sure the socket joins into the room. We can also store thesocket-channelId mapper in Redis as shown:

Redis (Remote Dictionary Server) is a fast, open-source, in-memory key-value data storage which can be used as a database. For instance, if your application has data stored in a HashMap, and you want to store that data in a data store – you can simply use the Redis hash data structure to store the data.

```tsx
socket.join(channelId);
```

```tsx
// SlackSocketClient/index.ts
/**
   * Post a message to Slack channel
   * @param {string} channel - channelId
   * @param {string} message - message to be posted
   * @memberof SlackClient
   */
  async postSlackMessage(channel: string, message: message, user?: any) {
    // TODO: If user does not have an avatar we need to show a generic image(instead of undefined)
    await this.slackWeb.chat.postMessage({
      channel,
      text: `${message}`,
      username: user ? `${user.firstName} ${user.lastName}` : 'ChatBot',
      icon_url: user
        ? user.picture
          ? user.picture
          : undefined
        : 'https://geekyants.s3.amazonaws.com/ChatBot_Avatar.png',
    });
  }
```

The channel will be created and messages sent from clients will be posted in Slack. When someone is responding to the Slack message, the Slack events are triggered and are handled through the defined route handler.

  • Slack events are triggered based on the actions being performed on the channel (i.e.,) channel creation, message,app_mention, channel_deleted, channel_archive, etc. We have to look for the specific event performed by specific user in order to handle the event. To do this:

      ```tsx
      // SlackSocketClient/index.ts
      export interface SlackEvent {
        client_msg_id: string;
        type: string;
        text: string;
        user: string;
        ts: string;
        team: string;
        blocks: any;
        channel: string;
        event_ts: string;
        channel_type: string;
        subtype: string;
        bot_id: string;
      }
      ```
    
  • Once we get the correct event with the proper payload, we can extract the message and send it back to the client using Socket. Since we used Slack's channelId as an identifier, this makes it easier to send it back to a particular socket, ignoring the sockets present in the room, like this:

      ```tsx
      // SlackSocketClient/index.ts
      import { io } from '../../../index';
    
      this.slackEvents.on('message', async (event: SlackEvent) => {
          // We want events only when a message is typed by a team member
          if (event.type === 'message' && !event.subtype && !event.bot_id) {
              io.of('/').adapter.allRooms((error: any, rooms: any) => {
                  if (rooms.indexOf(event.channel) >= 0) {
                      // Socket is active so send message to user
                      const { message, payload } = extractAndFrameMessage(event);
                      // Send the message to the socket.io "room"
                  io.to(event.channel).emit('TeamMessage', { message, payload });
                  }
              })
          }
      })
      ```
    
  • Similarly, we can handle multiple slack events. Slack provides many options for events, which can be customised, to integrate and use the Slack API for multiple purposes. There are chances, the socket connection might get closed if the mobile app is left in the background for a long time or the user hard closes the app. In which case, we check if the socket is present in the Socket Room using rooms.indexOf(event.channel) >= 0. If the socket is not present, we can trigger a push notification and re-initiate the socket connection by opening the notification on your mobile (provided that we have the Push Token in the DB).

The end result of integrating Live Support and ChatBot should look like:

Mobile.jpg

Sample.png

Transferring from ChatBot to Live Support

When the user asked for Live Support, we paused the ChatBot to hand over the chat to the Support Team. Now we need a way to return the control back to ChatBot. There are two ways to do this:

  1. The Client themselves can disconnect the Socket, which will automatically enable the ChatBot.

ChatEnd.png

  1. The Support Team can type a specific keyword through Slack Events to send a message and the payload to the client through a socket, which in turn resumes the ChatBot. Here, we need to mention the Slack Bot and type our keyword in order to trigger app_mention event and '@ChatBot resume'

    • Once we receive this event, we send a custom payload to let the client know he can resume the ChatBot.

        // SlackSocketClient/index.ts
        disconnectUser(socketId: string, event: SlackEvent) {
            const channelId = event.channel;
            const socket = io.sockets.connected[socketId];
            this.sendToUser(channelId, '', { resumeBot: true }, true);
            socket.disconnect(true);
        }
      

Issues and Fixes

  • In case there's a mismatch between the Socket IO and Socket IO Client version, make sure the socket server is able to communicate with the client's socket and vice-versa.

  • For smooth Slack integration, we need to provide proper permissions and scope accessories by subscribing to required events.

  • When enabling event subscriptions, make sure you provide your server app's URL to Slack Event Subscriptions to receive events occurring in Slack.

  • One of the major issues faced with the live support implementation is socket disconnection. We can fix that by mapping the Slack channel to SocketId and storing it in Redis.

Conclusion

In this era of virutalising every aspect of our lifestyle, this integration of communication through the ChatBot and Customer Support personnel adds a human touch for users. Google Dialog Flow was used for communication with the Bot while Slack with sockets was integrated for communicating with humans.

In a nut shell, this is one of my explorations on integrating various platforms for better internal and external operations. To know more, click here.

Hope you look forward to such similar articles.