Build a Customer Support Chat Bot with LUIS, React Hooks, Azure Serverless and Stream

Thierry S.
Thierry S.
Published January 14, 2020 Updated October 22, 2020

Initial Thoughts

This tutorial will teach you how to build your own customer support chat experience and create a serverless chatbot! The end result will look much like this:

The customer-facing chat experience is shown on the left, and the screen on the right shows the interface for support agents.

Here’s a GIF of the chatbot in action:

In order to help you achieve this end result and understand how you did so, we’ll walk you through the following sections:

Part 1 - React Chat Customer Service Chatbot
Part 2 - React Chat Agent View
Part 3 - LUIS Training for Your Chatbot
Part 4 - Webhook Integration with Stream
Part 5 - Serverless Azure Hello World
Part 6 - Serverless Webhook Handler Using Azure

The final code is stored in a GitHub repo; we encourage you to take the time to go through the process below in order to understand how to produce your very own chatbot, however, if you get stuck at any point during this tutorial you can look up the source code in the GitHub repo.

Part 1 - React Chat Customer Support Chatbot Tutorial

As a first step, we’re going to build the React interface that enables customers to ask questions. The end result of this part of the tutorial will look like this:

Setting up the frontend for customer support is easy. In this case, we’ll use React. If you want to use a different platform you’ll want to check out these tutorials: React Chat Tutorial, React Native Chat Tutorial, iOS/Swift chat Tutorial, and, Android Chat Tutorial.

Step 1: Create a New React App

Make sure you have node and yarn:

$ brew install node && brew install yarn

Create the chatbot-tutorial app:

$ mkdir chatbot-tutorial
$ cd chatbot-tutorial
$ yarn global add create-react-app
$ create-react-app chat-frontend
$ cd chat-frontend
$ yarn add stream-chat-react
$ yarn && yarn start

Visit http://localhost:3000 and you should see the default Create React App screen:

Step 2: Get A Stream API Key

  1. Visit Stream and register to get an API key.
  2. For the purpose of this tutorial, we’ll disable auth and permissions checks (note you should obviously not do this in a production app, but it allows you to focus on your chat experience instead of integrating your auth system). Open your Stream Dashboard, click your app, select “Chat” and disable authentication & permissions:

Important: You will need to click on your application and enable the “Disable Auth Checks” toggle as well as the “Disable Permissions Checks” toggle.

  1. Open your Stream Dashboard and copy your API key to your .env file:

You can create your .env file in your favorite editor and add the Stream API key. The full path of the file where you’ll want to add your API key is chatbot-tutorial/chat-frontend/.env. The line where you add your API to your .env file should be formatted as such:

REACT_APP_STREAM_API_KEY=YOUR_STREAM_KEY_HERE

Step 3: Edit App.js

As a first step, we’ll add a toggle button in the bottom right corner that prompts the user to click it to start or continue a chat session. To start, replace the code in src/App.js with the following code snippet:

import React from "react";
import {
  Chat,
  Channel,
  Window,
  MessageList,
  TypingIndicator,
  MessageInputFlat,
  MessageCommerce,
  MessageInput,
  ChannelContext,
  Avatar
} from "stream-chat-react";
import { StreamChat } from "stream-chat";

import "stream-chat-react/dist/css/index.css";

const chatClient = new StreamChat(process.env.REACT_APP_STREAM_API_KEY);

/**
 * A little button component to toggle the chat interface
 */
const Button = ({ open, onClick }) => (
  <div
    onClick={onClick}
    className={`button ${open ? "button--open" : "button--closed"}`}
  >
    {open ? (
      <svg width="20" height="20" xmlns="http://www.w3.org/2000/svg">
        <path
          d="M19.333 2.547l-1.88-1.88L10 8.12 2.547.667l-1.88 1.88L8.12 10 .667 17.453l1.88 1.88L10 11.88l7.453 7.453 1.88-1.88L11.88 10z"
          fillRule="evenodd"
        />
      </svg>
    ) : (
      <svg width="24" height="20" xmlns="http://www.w3.org/2000/svg">
        <path
          d="M.011 20L24 10 .011 0 0 7.778 17.143 10 0 12.222z"
          fillRule="evenodd"
        />
      </svg>
    )}
  </div>
);

function App () {
  const [open, setOpen] = React.useState(true);
  const [channel, setChannel] = React.useState(null);

  const toggle = () => {
    setOpen(!open);
  };

  return (
    <div className={`wrapper ${open ? "wrapper--open" : ""}`}>
      <Button onClick={toggle} open={open} />
    </div>
  );
}

export default App;

Note: At first, you will get warning messages about unused imports; to keep things simple, we included all imports now so that it is easier to follow this tutorial. Once our app is completed, you should not see these warnings again.

After you’ve updated your App.js file, open src/index.css and replace the CSS with the following:

html,
body {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
*,
*:after,
*:before {
  box-sizing: inherit;
}

#root {
  width: 100%;
  height: 100vh;
  display: flex;
  justify-content: flex-end;
  background: url('https://images.pexels.com/photos/1421903/pexels-photo-1421903.jpeg') top left no-repeat;
  background-size: cover;
}

.str-chat {
  height: auto;
  max-height: 500px;
}

.str-chat-channel {
  max-height: 0 !important;
  min-height: 500px;
  width: 375px;
}

.str-chat-channel .str-chat__container {
  height: auto;
  max-height: 500px;
  border-radius: 10px;
  max-width: 375px;
}

.wrapper {
  display: flex;
  flex-direction: column;
  position: fixed;
  right: 20px;
  top: 60px;
  align-items: flex-end;
  justify-content: flex-end;
  height: 100%;
  height: calc(100% - 40px);
  padding: 30px 0;
}

.wrapper--open .str-chat__container {
  height: 100%;
}

.button {
  height: 60px;
  width: 60px;
  margin-top: 16px;
  border-radius: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 60px;

  background: #ffffff;
  background-image: linear-gradient(
    -180deg,
    rgba(255, 255, 255, 0.1) 0%,
    rgba(0, 0, 0, 0.1) 100%
  );
  border: 1px solid rgba(255, 255, 255, 0.18);
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.5),
    inset 0 1px 0 0 rgba(255, 255, 255, 0.24);
  cursor: pointer;
}

.button svg {
  fill: #000;
  opacity: 0.8;
}

.button--closed svg {
  position: relative;
  left: 3px;
  fill: #0043f7;
}

.button:hover svg {
  opacity: 1;
}

By now, your chat experience should look like this:

You can toggle the button, but it doesn’t do anything just yet. Note how clean the App.js code is, as a result of using React Hooks!

Step 4: Guest User Interface

For the purpose of this demo, we will ask the user to provide their name and email. Stream Chat supports 3 types of user authentication, and it depends on your use case as to which one works best:
Regular users (Use this if the user asking support questions is already logged in – especially for in-app support it’s common for users to be logged in)
Guest Users (Allow users to set a guest name and email)
Anonymous Users (Allow users to chat without providing any details)

To begin the process of gathering information to authenticate the user, go ahead and open up src/App.js and add the GuestUserInput component to the file (just above your App class:

/**
 * A little interface for the user to set up their name and email before
 * The chat starts.
 */
export function GuestUserInput({ setChannel, ...props }) {
  const [name, setName] = React.useState("");
  const [email, setEmail] = React.useState("");

  const handleSubmit = async event => {
    event.preventDefault();
    const userID = window.btoa(email).replace(/=/g, "");
    // in a real app you would do some round robin on active agents..
    const assignedSupportAgent = "support-agent-123";
    const user = await chatClient.setGuestUser({
      id: userID,
      name: name,
      email: email
    });
    const channel = chatClient.channel("commerce", userID, {
      members: [chatClient.user.id, assignedSupportAgent],
      assigned: assignedSupportAgent,
      status: "open"
    });
    channel.watch({ presence: true });

    setChannel(channel);
  };

  return (
    <div className="str-chat str-chat-channel commerce light">
      <div className="str-chat__container">
        <div className="str-chat__main-panel">
          <div className="str-chat__header-livestream">
            <div>
              Hi, feel free to ask any questions or share your feedback. We're
              happy to help!
              <Avatar image="https://pbs.twimg.com/profile_images/897621870069112832/dFGq6aiE_400x400.jpg" />
              <Avatar image="https://i.pravatar.cc/300" />
              <Avatar image="https://i.pravatar.cc/200" />
            </div>
          </div>
          <div className="str-chat__list ">
            <div>
              Hi, what's your name and email?
              <form onSubmit={handleSubmit}>
                <label>
                  Name:
                  <input
                    type="text"
                    value={name}
                    onChange={event => {
                      setName(event.target.value);
                    }}
                  />
                </label>
                <br />
                <label>
                  Email:
                  <input
                    type="text"
                    value={email}
                    onChange={event => {
                      setEmail(event.target.value);
                    }}
                  />
                </label>
                <br />
                <input type="submit" value="Submit" />
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

Next, we need to change the App component to render the GuestUserInput and the chat interface. We’ll update our App component to look like this:

function App() {
  const [open, setOpen] = React.useState(true);
  const [channel, setChannel] = React.useState(null);

  const toggle = () => {
    setOpen(!open);
  };

  function renderChat() {
    return
  }

  let nodes = "";

  if (open) {
    if (channel) {
      nodes = renderChat();
    } else {
      nodes = <GuestUserInput setChannel={setChannel} />;
    }
  }

  return (
    <div className={`wrapper ${open ? "wrapper--open" : ""}`}>
      {nodes}
      <Button onClick={toggle} open={open} />
    </div>
  );
}

By now, your chat experience should show you an interface to set up your guest user:

If you submit your details, you’ll get an API error logged, since we didn’t create a user account for the support agent yet; let’s do that now!

Step 5: Create a User Account for the Support Agent

Start by installing the getstream-cli:

sh
1
yarn global add getstream-cli

Next, configure your account:

sh
1
stream config:set

It will automatically ask you for some configs. Be sure to use your own API key and secret:

And, finally, create the user with a userID of “support-agent-123”:

sh
1
stream chat:user:create -u support-agent-123 --role user

Step 6: Render a Chat Interface

You will notice that you see an empty screen after entering your details. As a next step, we’ll add the actual chat components.

Replace the dummy renderChat function in your App component in src/App.js with the following:

function renderChat() {
  return (
    <Chat client={chatClient} theme={"commerce light"}>
      <Channel channel={channel}>
        <Window>
          <MessageList
            TypingIndicator={TypingIndicator}
            Message={MessageCommerce}
          />

          <MessageInput Input={MessageInputFlat} />
        </Window>
      </Channel>
    </Chat>
  );
}

You’ll now see a functional chat interface when you fill in your user details:

Note how easy it is to build a chat interface by leveraging the Channel, MessageList and MessageInput components. All of these are fully customizable, and you can write your own if you want to! Have a look at the React Chat Tutorial to learn more.

Step 7: User Presence & Showing Who Is Online

It would be nice to show a ChannelHeader that shows who is currently online... So, let’s create a custom ChannelHeader component called MyChannelHeader:

/**
 * A custom channel header element which shows who is currently online
 */
function MyChannelHeader() {
  const channelContext = React.useContext(ChannelContext);
  const channel = channelContext.channel;
  const client = channelContext.client;
  const [members, setMembers] = React.useState(channel.state.members);

  React.useEffect(() => {
    function handleUserPresenceChange(event) {
      setMembers(channel.state.members);
    }

    client.on("user.presence.changed", handleUserPresenceChange);

    return function cleanup() {
      client.off("user.presence.changed", handleUserPresenceChange);
    };
  });

  const onlineUsers = [];
  if (members) {
    for (let m of Object.values(members)) {
      if (m.user.online) {
        onlineUsers.push(m.user);
      }
    }
  }

  if (!onlineUsers.length) {
    return (
      <div className="str-chat__header-livestream">
        Sorry, nobody is online at the moment. A support agent has been
        notified.
      </div>
    );
  }

  return (
    <div className="str-chat__header-livestream">
      Currently online:
      {onlineUsers.map((value, index) => {
        return (
          <div key={index}>
            <Avatar image={value.image} name={value.name} />
            {value.name}
          </div>
        );
      })}
    </div>
  );
}

Next we’ll add the MyChannelHeader to the renderChat function. The updated function now looks like this:

function renderChat() {
  return (
    <Chat client={chatClient} theme={"commerce light"}>
      <Channel channel={channel}>
        <Window>
          <MyChannelHeader />

          <MessageList
            TypingIndicator={TypingIndicator}
            Message={MessageCommerce}
          />

          <MessageInput Input={MessageInputFlat} />
        </Window>
      </Channel>
    </Chat>
  );
}

Run yarn start in the chat-frontend directory and visit http://localhost:3000. You’ll now see a functional chat experience with a header which shows who is currently online:

If you got stuck in one of these steps, you can also copy and paste the end result from the GitHub repo’s AppStep7.js.

Part 2 - Dashboard for the Live Chat

For the agent, we want an interface that allows them to productively talk to many people at once. To begin to create this, we’ll start with a Create React App app, again:

Step 1: Create a New React App

Make sure that you are in the chatbot-tutorial directory, and run:

$ create-react-app chat-agent-dashboard
$ cd chat-agent-dashboard
$ yarn add stream-chat-react
$ yarn && yarn start

Step 2: Create Your .env.development

Copy your .env file from the frontend project:

sh
1
cp ../chat-frontend/.env .env

The API key is the same as you used in Part 1. In the case that you’ve forgotten what it was, head over to Stream’s dashboard to look it up again (or check the .env file you created in the last section).

Step 3: Edit src/App.js

Open up chat-agent-dashboard/src/App.js and replace the contents with:

import React from "react";
import {
  Chat,
  Channel,
  ChannelHeader,
  Thread,
  MessageList,
  ChannelList,
  TypingIndicator,
  MessageInputFlat,
  MessageCommerce,
  MessageInput,
  Window
} from "stream-chat-react";
import { StreamChat, DevToken } from "stream-chat";

import "stream-chat-react/dist/css/index.css";

const chatClient = new StreamChat(process.env.REACT_APP_STREAM_API_KEY);

chatClient.setUser(
  {
    id: "support-agent-123",
    name: "Jessica",
    image: "https://getstream.io/random_svg/?id=support-agent-123&name=Jessica"
  },
  chatClient.devToken("support-agent-123")
);

const filters = { type: "commerce" };
const sort = { last_message_at: -1 };
const channels = chatClient.queryChannels(filters, sort, {
  watch: true,
  presence: true
});

function App() {
  return (
    <div>
      <Chat client={chatClient} theme={"commerce light"}>
        <ChannelList
        filters={filters}
        sort={sort}
        />
        <Channel>
          <Window>
            <ChannelHeader />

            <MessageList
              TypingIndicator={TypingIndicator}
              Message={MessageCommerce}
            />

            <MessageInput Input={MessageInputFlat} />
          </Window>
        </Channel>
      </Chat>
    </div>
  );
}

export default App;

Step 4: Run yarn start

Be sure to run yarn start in the chat-agent-dashboard directory and visit http://localhost:3001. Your agent interface will now look like this:

Note: If you get an error about an invalid token, be sure to disable auth and permissions checks in the dashboard.

Some cool features to note:

  1. File Uploads

You can upload files to the chat via drag & drop, copy/pasting or manually selecting a file:

  1. Rich URL Previews

If you post a URL, you’ll see a rich preview in the chat interface:

  1. Typing Indicators

When either user is typing, a dot animation will pop up to indicate this to the opposite party:

  1. Online Status for Agent and Customer

The customer will be able to see which agents are online at the top of the interface:

Part 3 - LUIS Azure Training for Your Chatbot

To begin setting up the questions/statements your chatbot will be able to address and the responses it should have, head over to https://www.luis.ai/ to set up your Language Understanding Service (LUIS). LUIS analyzes messages and tells you what the intent is. Here are some examples:

For the purpose of this blog post, we’ll teach LUIS to recognize the “restaurant reservation” intent and we’ll teach it the answer to life the universe and everything.

Step 1: Create an App

Sign in to the LUIS dashboard and create an application. Once logged in, click “Create new app” as shown in the screenshot below:

When prompted, specify the following for your chatbot (and, then, click “Done”):

Name: ChatbotTutorial
Culture: English

Step 2: Create a New Intent

As a next step, we’ll create a new Intent called “Answer”, inspired by the amazing Hitchhiker's Guide to the Galaxy. Click on the “Build” button at the top right, and then click on “Create new intent”:

You will be prompted for the intent name (“Answer”). Once loaded, specify your “utterances” to LUIS:

  • What’s the answer to life the universe and everything
  • What’s the answer
  • The answer please
  • Tell me the answer
  • What’s the meaning of life
  • What’s the answer to the universe

Step 3: Leverage a Built-In Intent

We’ll also add a built-in Intent. Click on the Intents button to take you to the previous page. From there, click the “Add prebuilt domain intent” button.

Building your own app? Get early access to our Livestream or Video Calling API and launch in days!

Select the RestaurantReservation.Reserve intent and then click the “Done” button.

Step 4: Train the Model

Now that we’ve updated the settings, we’ll want to train the model. Hit the “Train” button in the top right corner:

Step 5: Publish the Model to Production

Hit the “Publish” button and select Production. This is important; if you select “Staging”, the next steps in the tutorial will fail. Wait a few seconds for your model to be published.

In the meantime, let’s take note of 3 settings... To access them, click Manage -> Application Information:

  1. Your application ID:
  1. Your primary key and (3) region:

Part 4 - Learn How to Use Stream Webhooks

As a next step, we’ll connect Stream’s webhook to Azure. To do this, we’ll leverage the Stream CLI.

Step 1: Verify Stream-CLI is installed

Verify that you have Stream’s CLI installed (you used it to configure the user in Part 1 of this tutorial):

sh
1
$ stream version

If you don’t have it installed yet, you can install the CLI by running the following yarn command:

sh
1
$ yarn global add getstream-cli

And set up your API key like this:

sh
1
$ stream config:set

Step 2: Test the Webhook

To test the webhook integration, we’ll create a new request bin (it doesn’t need to be private).

Next, configure your new request bin as your webhook URL for Stream. Be sure to replace the URL in the following command with the request bin URL you’ve just created:

sh
1
$ stream chat:push:webhook --url https://enfspya8mur9f.x.pipedream.net

If you visit http://localhost:3000 and leave a chat message you should see the message.new event shows up in the request bin:

Awesome! Now that you’ve learned how to set up Stream’s webhooks, we’ll continue to the serverless part of this tutorial.

Part 5 - Serverless Azure Chat

We’ll want to leverage an Azure Function app to connect Stream & LUIS. The function needs to do these 3 things:

Parse the Stream webhook data
Run intent analysis with LUIS
Send a reply on the channel if we recognize the intent

Follow along with the steps below to get your Azure Function up and running:

Step 1: Set Up Azure

Go to https://portal.azure.com and set up your account. You get $200 in credits when you sign up so you can complete this tutorial without setting up any billing. Microsoft recently changed Azure to ask you to verify your phone number & credit card in the next step. (which is a bit annoying...)

Step 2: Create a Function App

Search for “Function App” and select it:

Click Add and once on the next screen, you’ll want to select the following:

Function App Name: ChatbotTutorial
Runtime Stack: Node

and click Review & Create.

Now, hit Create to spin up your function app! This takes anywhere from a few seconds to a few minutes.

Your function app should now be provisioned:

Great! Now that we’ve created our function app, it’s time to write a little bit of code:

Step 3: Verify Azure Functionality

Make sure you have azure functions core tools up and running:

sh
1
$ yarn global add azure-functions-core-tools

You can verify it worked by running func version. As of January 2020, the current version is 2.7.1948.

Step 4: Set Up Serverless Directory

Set up your serverless directory using the following shell commands:

sh
1
2
3
$ mkdir serverless $ cd serverless $ func init --worker-runtime node --language javascript

Step 5: Create New Message Handler Function

In this next step, we’ll create a handler function called NewMessageTrigger:

sh
1
$ func new -t "HTTP trigger" --name "NewMessageTrigger"

Step 6: Test New Message Handler Function

Next, we’ll want to test our NewMessageTrigger function locally:

sh
1
$ func start

Note: If you receive an error about your version of Node.js, you can fix that using the following installing node v10.14.1 or by using NVM as shown below.

sh
1
2
3
4
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash $ nvm install 10.14.1 $ nvm alias default 10.14.1 $ yarn install

You should now be able to try out your local function by visiting:

http://localhost:7071/api/NewMessageTrigger?name=John

Part 6 - Serverless Webhook Handler with Azure

Ok, now that we’ve got the basics in place, let’s connect all the dots! Remember, we want to accomplish the following:

Handle the incoming data from Stream’s webhook platform
Run intent analysis using Azure’s LUIS
Reply to the user in the chat

Step 1: Create Your Serverless Project

Go to the serveless folder and install the dependencies:

sh
1
$ yarn add stream-chat axios dotenv

Step 2: Set Up Your Function Handler

Replace the code in NewMessageTrigger/index.js with the following:

const axios = require("axios");
const StreamChat = require("stream-chat").StreamChat;
require("dotenv").config();

/**
 * getStreamClient - returns the Stream Chat client
 *
 * @returns {object}  Stream chat client
 */
function getStreamClient() {
  const client = new StreamChat(
    process.env.STREAM_API_KEY,
    process.env.STREAM_API_SECRET
  );
  return client;
}

/**
 * analyseIntentWithLUIS - runs intent analysis on the message with LUIS
 *
 * @param  {string} messageText the message text to analyse
 * @returns {object}             returns the top intent as {intent: 'name', score: 0.9}
 */
async function analyseIntentWithLUIS(context, messageText) {
  const appID = process.env.LUIS_APP_ID;
  const key = process.env.LUIS_SUBSCRIPTION_KEY;
  const region = process.env.LUIS_REGION;

  const url = `https://${region}.api.cognitive.microsoft.com/luis/v2.0/apps/${appID}?subscription-key=${key}&q=${messageText}`;

  try {
    const response = await axios.get(url);
    const data = response.data;
    return data.topScoringIntent;
  } catch (e) {
    context.log(`Failed to analyse intent with luis, error ${e.response.data}`);
    throw e;
  }
}

/**
 * anonymous function - This request handler does a few things
 *
 * 1. Handle the Stream webhook data
 * 2. Run intent analysis with LUIS
 * 3. Send a reply on the channel
 */
module.exports = async function(context, req) {
  // show a nice error if you send a GET request
  if (req.method === "GET") {
    context.res = {
      body: { error: "Invalid request, only POST requests are allowed" }
    };
    return;
  }
  // important: validate that the request came from Stream
  const chatClient = getStreamClient();
  const valid = chatClient.verifyWebhook(
    req.rawBody,
    req.headers["x-signature"]
  );
  if (!valid) {
    context.res = {
      body: { error: "Invalid request, signature is invalid" }
    };
    return;
  }

  // reply to message.new, but not to messages written by the bot
  // (you get an interesting loop if you don't exclude bot messages)
  if (req.body.type === "message.new" && req.body.message.user.id !== "mrbot") {
    // parse the stream webhook format
    const messageText = req.body.message.text;
    const cID = req.body.cid;
    const channelType = cID.split(":")[0];
    const channelID = cID.split(":")[1];

    // run intent analysis with LUIS
    context.log("starting to analyse intent with LUIS");
    const topIntent = await analyseIntentWithLUIS(context, messageText);
    const intent = topIntent.intent;
    const score = topIntent.score;
    context.log(
      `Received a message.new with text "${messageText}" and found intent ${intent} with score ${score}`
    );

    // if we understand this intend, send a reply
    const channel = chatClient.channel(channelType, channelID);
    const botUser = { id: "mrbot", name: "MR Bot" };

    if (intent === "Answer") {
      await channel.sendMessage({
        text: "42 is the answer",
        user: botUser
      });
    } else if (intent === "RestaurantReservation.Reserve") {
      await channel.sendMessage({
        text: "Great idea, I'm hungry",
        user: botUser
      });
    }

    // send a 200 response with some extra info
    context.res = {
      body: { messageText: messageText, intent: intent, score: score }
    };
  } else {
    let userID = null;
    if (req.body.message && req.body.message.user) {
      userID = req.body.message.user.id;
    }
    const msg = `Skipping request of type ${req.body.type} from userID ${userID}`;
    context.log(msg);
    context.res = {
      body: { error: msg }
    };
  }
};

Let’s take a moment to understand the above code... We start by receiving the request; the body of the request (req.body) has the information from Stream’s webhook. When we receive an event of type message.new, and (this is important) the message is not written by a bot, we run intent analysis via LUIS. If we understand the intent, the bot replies on the channel.

Step 3: Create a Local .env File

Create a file called .env, the full path is serverless/.env with the following settings:

STREAM_API_KEY=YOUR_STREAM_API_KEY
STREAM_API_SECRET=YOUR_STREAM_API_SECRET
LUIS_APP_ID=YOUR_LUIS_APP_ID
LUIS_SUBSCRIPTION_KEY=YOUR_LUIS_SUBSCRIPTION_KEY
LUIS_REGION=westus

Step 4: Configure Your Production .env

To go live, we’ll want to configure those same environment settings in Azure’s web interface. First click “Go to resource”:

Next click “Function app settings”:

And select “Manage application settings”:

On this page you can add the same 5 environment variables you’ve just added to your serverless/.env file:

Login to Azure using the following command:

sh
1
$ az login

You will be prompted to authenticate via your browser as shown in the screenshot below (this is really well done):

And deploy the function using this command:

sh
1
$ func azure functionapp publish ChatbotTutorial

Note: If you run into an error, ensure that you have installed the azure-cli:

sh
1
$ brew update && brew install azure-cli

Note: The invocation URL will be provided in the response body after publishing your function (as shown in the above screenshot).

When you run the above command, you’ll receive an “invoke url”; you’ll want to update your webhook URL to point to your invocation URL a shown below:

sh
1
$ stream chat:push:webhook --url yourinvokeurl

After following this last step, your bot will be fully connected to the chat interface! Try it out by typing something such as:

“What’s the answer?”
OR
“make a reservation”

In the chat interface:

A side note on Debugging with NGROK

Webhooks can be a little bit hard to work with. A tool that helps with local debugging is NGROK, which gives you a public URL for your local endpoint.

To utilize NGROK, first, ensure that your server is running by running:

sh
1
func start

Next, install NGROK by following the instructions on their site:

https://ngrok.com/download

Then, expose your local web server using ngrok:

sh
1
$ ngrok http 7071

This makes it easier to test changes (rather than you having to deploy to Azure while you’re coding). Let’s see if it works by configuring Stream’s webhook:

sh
1
$ stream chat:push:webhook --url http://<YOUR_NGROK_URL>/api/NewMessageTrigger\?name\=thierry

Concluding the Chatbot Tutorial

After following the above tutorial you should have a fully functional chatbot for your application! It should look very similar to this:

If at some point you got stuck, head over to the GitHub repo to check out the end result of this tutorial and compare to the code you’ve written.

Additionally, this tutorial used React for the user interface. Have a look at the React Native Chat Tutorial, iOS/Swift Chat Tutorial or Android/Kotlin Chat Tutorial, if you want to use a different platform!

To learn more you might want to check out:

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->