DEVELOPMENT

TECH

How to Make a Chatbot Using JS

Updated: {{date}}

{{time}} min read

Contents

Summarize this article with AI

Imagine yourself in a world where all you have to do to interact with a company is drop a line to an intelligent humanoid who can resolve all your problems, have a chat with you, and crack a couple of jokes while handling any of your requests. Sounds cool, right?

Well, we're not in that world. But we are in a similar one thanks to chatbots (who aren't so great at cracking jokes, alas). They are actually useful in various tasks from making an appointment with a doctor to persuading your robot vacuum not to chase your cat.

According to this research, businesses can save up to 30% on serving customer requests with a chatbot. That's because you no longer have to hire humans to do repetitive tasks like answering basic questions or handling monotonous requests. A chatbot will do it way quicker and cheaper.

But for your chatbot to be truly responsive and human-like, it needs a couple of things prehistoric people (<2010s) didn't have β€” AI and Machine Learning (ML). They allow the chatbot to perform operations that cannot be described as a simple algorithm or a sequence of actions, therefore providing a greater degree of personalization.

So, in case you're willing to integrate a chatbot into your business processes β€” you're welcome!

Feel free to jump to sections 4 and 5 if you're interested in the technical side of the issue to see how we implement a chatbot using Node.js.

πŸ€– Rule-based Chatbot VS AI Chatbot Development

When talking about chatbots, it's important to understand the difference between a rule-based and AI-powered chatbot. A rule-based chatbot has pre-written options for a user to choose from, meaning that you can't type anything that's not on the list.

You can think of it as a bunch of buttons on a player, where you can’t actually press anything but the buttons themselves.

Why don't we take a look at the pros and cons of a rule-based chatbot:

An AI-powered chatbot functions in a completely opposite way. As the name suggests, they use AI, Machine Learning and Natural Language Processing to perform their tasks. With them, you can write any text and they, ideally, will be able to recognize your β€œintent”, or rather what kind of response you want to hear from them. The output (answer) of AI-powered chatbots may be generated from scratch, or be created from a template β€” just as with the rule-based bots.

The pros and cons of an AI-powered chatbot:

But in case you really like some features of both an AI and a rule-based chatbot, you can get the best of both worlds by building a hybrid chatbot. It will generally use rule-based patterns but also rely on Machine Learning for complex tasks such as sentiment analysis or handling textual requests.

Read our guide on ChatGPT integration.

❓ Why Companies Build a Chatbot: Use Cases

Companies use chatbots for a wide range of purposes like automating appointments booking or making personalized offers. In this article, we'll talk about the most common use cases of AI chatbots.

Making Appointments & Booking

Let's say you have a fitness or healthcare business where your users have to make some kinds of appointments. In that case, AI chatbots may come in handy as they would be much cheaper than a real person answering the calls, but also way more flexible compared to rule-based chatbots, being able to handle new situations, like answering a question it has never seen before.

If you're particularly interested in Booking Apps, we have a special article dedicated to developing an online booking system for applications or websites:

Making Personalized Recommendations

In case you offer users a variety of products, you could integrate a Recommendation System into the AI chatbot. Such a system can provide users with a personalized approach and give recommendations based on their previous searching & buying history.

Receiving Voice Inputs & Giving Voice Outputs

If you'd like to improve the accessibility of your chatbot, adding voice Inputs & Outputs might turn out to be a great idea. For example, this could be useful for interacting with an IoT device, which may not actually have a keyboard or a display. The most popular example can be Alexa from Amazon.

Managing a Few Apps & Services

Let's imagine that your business uses a whole suite of different apps and/or services to deal with different tasks. So that your users don't have to constantly switch between them, you can make one point of access to all of them in your chatbot. For example, if your company has different apps for task management, attendance tracking, and planning, you can set up one chatbot and connect to all those services through their APIs.

βš™οΈ How to Create a Chatbot: Tech Insights

To better understand the logic behind chatbots development, let's take a look at how they function.

(Pre-) Processing stage

The first step in the workflow of any text-based ML application is preprocessing. We need it because computers aren't able to understand the logic behind sequences of characters (such as this article) or sounds (such as a recorded message) β€” they only perceive numbers. For that reason, we need to somehow turn texts or voice requests into a bunch of numbers.

In case we have voice input, we first have to convert it into text. For this task we can use open-source solutions such as Kaldi or PocketSphinx for mobile devices. Alternatively, if we don't want to run the speech recognition process on our own server, we can use third-party APIs such as Google's Speech-to-Text or Amazon Transcribe.

The first step in preprocessing is tokenization, which is defining the boundaries between tokens (i.e. words in ML lingo). Then, we have to extract entities β€” it's usually called Named Entity Recognition (NER). This will allow the chatbot to understand that words like β€œNew York” or β€œthe day after tomorrow” are entities, thus, they function as single units in a sentence and should not be separated.

The last important step in preprocessing is Word Embedding. This will actually allow us to substitute separate words with huge numeric vectors. The way word embeddings (vectors that correspond to words) are calculated isn't that important to understand this issue, but the most popular algorithm is Word2Vec. All these steps can be done in a few lines of code using such libraries as Python's SpaCy or Node's Natural/compromise.

By this point, we should already have all the text converted into a matrix of size (embedding dimension x number of words), which means we are ready to crunch these numbers to understand what the user wants from the chatbot.

Understanding the Intent of a User

An intent in chatbot architecture describes what the user wants the chatbot to do with their message. For example, when you ask your friend β€œHow've you been?”, you expect them to share their recent news. In a similar way, when you ask a chatbot β€œCan I have a meeting with a doctor tomorrow?”, you expect it to either book an appointment for you, or reject the query in case there's no time available and offer another option.

This idea of having discrete intents combines well with the ML task of classification. The point of it is to classify the input data into two or more discrete categories, e.g. tell whether an email is spam or not. In our case with chatbots, we wish to classify a user message into its intent. This is nicely represented by the following graph:

Here, you can see how the embeddings are input in the first layer and then are forward-propagated, or fed in, through the many connections that are between the layers of the network. When the embeddings reach the final, or output layer, they choose the most likely intent. This intent is passed on to generate the response of the model.

Creating Fully Connected Neural Networks like the one above is pretty easy, and almost any ML library should provide ways to build them. Some of the most popular ML frameworks for this and other tasks are PyTorch, Tensorflow for JS / Python, and mlpack for C++.

Generating a Response

There are a few approaches to generate a response for a given user message.

The first one is to use a generative language model, such as GPT-3 or a simpler Recurrent Neural Network (RNN). But the problem with this approach when integrating chatbots is that it is either unnecessarily complicated (say hello to GPT-3 with 175 billion parameters) or not good enough (RNN's quality of generated text leaves much to be desired). This approach won't use the intents, and will just write the answer based on the message itself. Generating a message from scratch is a very complex task which requires models trained on loads of textual data, which most people don't have.

Another approach is to have generic pre-written messages that correspond to every intent. This kind of model will be much more lightweight and fit well into chatbots' tasks. But it also enables a higher level of customization. For example, the pre-written messages may have placeholders for the user's name, named entities, or any variables at all. Thus, you achieve relatively customizable answers while keeping the model rather simple.

After getting the response in a textual form, you might optionally want to convert it into speech. For this task, there's a number of speech recognition APIs, such as Google's Text-to-Speech or Amazon Polly.

βœ… AI Chatbot Implementation

After we talked about how chatbots function on the inside, let's take a look at the process of chatbot implementation.

The easy way: No-Code Platforms

If you don't want to get your hands dirty with writing a chatbot from scratch, you can choose one out of many available chatbot platforms. The most popular ones are Dialogflow (offers a $600 trial) and Wit.ai (free). They both provide a clear and intuitive UI for creating intents, adding named entities, setting up logical flow between different intents, etc. Such chatbots use ML.

They are also a great option if you don't want to run your bot on your server. You can use their APIs to get answers to users' requests. For example, connecting to Wit.ai in Node.js to get a response is as easy as this:

const { Wit } = require('node-wit');

const client = new Wit({
  accessToken: MY_TOKEN,
});

console.log(client.message('Make a reservation at 6am tomorrow'));

However, there might be reasons for deciding against any of the platforms:

  • Security. If you use a platform, you can't make sure that no one else has access to the data your users pass to the chatbot.
  • Prices. Most of the platforms utilize a subscription-based model. It means that in a long-term run it may not be the most cost-efficient solution.
  • Lack of flexibility. If you build a chatbot from scratch, you can use literally any tool the programming world has, while platforms usually limit the choice.

If any of these reasons resonate with you, or you just feel like building a chatbot from scratch (which is certainly fun), check out the next section.

The harder way: Node + NLP.js

In order to implement a chatbot from scratch, we first have to choose an NLP/ML framework to process the text and create a neural network. Since this article focuses on Node implementation of chatbots, NLP.js is a good choice for this task.

Moreover, gaining experience with tools like NLP.js can enhance your portfolio and open up opportunities in Node.js jobs, where chatbot development skills are often in demand.

It has the following features:

  • Custom entities in addition to the built-in ones like dates, names, etc.
  • Built-in fully connected neural networks for classification.
  • Language recognition.
  • Tokenization, stemming, and other basic NLP tasks.

Our chatbot will have the following features:

  • Saying hello, goodbye, giving info about the pricing according to corresponding intents.
  • Booking a meeting with a doctor by means of extracting the date & time, and the name of the doctor.
  • Recognizing custom entities, such as doctors’ names.
  • Parsing dates in natural language format, i.e. not only in a standardized way like 20.05.2021, but also with queries like β€œthe day after tomorrow” or β€œnext Monday”.

Here are versions of the packages we're going to use:

"@nlpjs/basic": "^4.22.0",
"dotenv": "^9.0.2",
"node-nlp": "^4.22.1",
"sugar": "^2.0.6",
"telebot": "^1.4.1"

First of all, we have to create a corpus, i.e. a dataset of question-answer entries, as well as of all the entities required. We decided not to add too many intents to keep it simple:

{
  "name": "Corpus",
  "locale": "en-US",
  "data": [
    {
      "intent": "greetings.bye",
      "utterances": [
        "goodbye for now",
        "bye bye take care",
        "okay see you later",
        "bye for now",
        "i must go"
      ],
      "answers": [
        { "answer": "Till next time" },
        { "answer": "see you soon!"}
      ]
    },
    {
      "intent": "user.redirect",
      "utterances": [
        "Can I talk to a real human?",
        "Please connect me to a living person",
        "Please redirect me to an employee",
        "Can I talk to someone human?",
        "I want to enter the live chat",
        "I hate chatbots actually so please could you get out of my screen and connect me to someone made from flesh thanks"
      ]
    },
    {
      "intent": "user.thanking",
      "utterances": [
        "thanks",
        "thank you",
        "thanks a billion",
        "thx",
        "grateful"
      ],
      "answers": [
        { "answer": "No problem!"},
        { "answer": "Sure!"}
      ]
    },
    {
      "intent": "user.pricing",
      "utterances": [
        "Can I see the prices?",
        "What's the pricing?",
        "How much is it?",
        "How much do I have to pay?"
      ],
      "answers": [
        { "answer": "Check out our pricing by the link:\nhttps://stormotion.io/"}
      ]
    },
    {
      "intent": "greetings.hello",
      "utterances": [
        "hello",
        "hi",
        "howdy",
        "hey",
        "greetings",
        "hey there",
        "hola",
        "What's up?"
      ],
      "answers": [
        { "answer": "Hey there!" },
        { "answer": "Greetings!"}
      ]
    },
    {
      "intent": "user.book",
      "utterances": [
        "I'd like to book an appointment",
        "I wanna see the doc",
        "Can I see the doctor?",
        "When is the doctor available?",
        "I want some help with my knee",
        "I want to see @doctor",
        "I'd like to visit @doctor",
        "Is @doctor available?",
        "another reservation pls"
      ]
    }
  ],
  "entities": {
    "doctor": {
      "options": {
        "christine": ["Christine", "Christie", "Christine Collins", "The woman with black hair", "the female doctor"],
        "josh": ["Josh", "Joshua", "Josh Stammer", "The main with glasses"],
        "abraham": ["Abraham", "Abraham Brown", "Abe", "The man with brown curly hair"]
      }
    }
  }
}

Here you can see the intents with their corresponding expressions that the bot will use to train itself. Once the bot recognizes an intent, it will randomly choose one of the given answers. Some of the intents don't have their corresponding answers because those cases are more complex and we have to use a callback function to process them.

Also, you can see our custom entity type β€” β€œdoctor”. There are three doctors available and their alternative names (so that the bot understands that β€œChristine” and β€œChristie” refer to the same person).

Next, we have to set up our index.js file where we'll run trainings and establish the connection with Telegram's API. For the second task, we will use TeleBot, which is a very lightweight package, just what we need.

"use strict"
import { NlpManager } from "node-nlp";
import TeleBot from "telebot";
import dotenv from "dotenv";
dotenv.config()

const manager = new NlpManager({ languages: ['en'], forceNER: true });
const bot = new TeleBot(process.env.BOT_TOKEN);

// Load the model. Then connect to the Telegram bot
(async() => {
  manager.addCorpus("corpus.json");
  await manager.train();
  manager.save()

  bot.on('text', async function(msg) {
    let response;
    let chatid = msg.chat.id;

    if (msg.text === "/start") {
      await msg.reply.text("Hello! My name's Booker. How can I help you?");

    } else {
      response = await manager.process('en', msg.text);
 
      await msg.reply.text(response.answer);
    }
  });

  bot.start();

})();

First, we import the required packages, set up the environment variables with the token of the bot in .env and create two instances of two major classes: NlpManager and TeleBot. The first will be used to create neural networks, train them based on our defined corpus, extract named entities and give answers. The second one will connect the chatbot to Telegram's API to exchange messages between the user and the bot.

The rest of the code defines the unnamed function, which is our main loop where all the training and message exchange is happening.

If you run this code, you'll first see a training log of our NN, and then the message of the bot being started. By this time, you should be able to text the bot whose token you've provided in .env, and it will respond to you.

But there's one feature that we haven't implemented yet: the actual booking itself. You can remember that we haven't provided any answers for β€œuser.book” intent, so even if the bot does recognize it, there will be no answer. To fix this, we will create a whole custom class called User. Its main task will be to enable the booking functionality, i.e. asking for and recognizing the name of the doctor, as well as date & time of the reservation.

So that the chatbot understands natural language dates such as β€œthe day after tomorrow” or β€œon the last day of May”, we will use Sugar, which extends the native Date object of Javascript, giving it additional functionality such as parsing strings.

Also, we will define an object β€œavailableDoctors”, which is meant to imitate a system with dynamic time slots. As this article doesn't focus on creating such a system, a demo JS object with doctors will do just fine.

The main structure of our User class looks like this:

import Sugar from "sugar";

Sugar.Date.extend()

// Imitate a system with a dynamic time slots for doctors
const availableDoctors = { 
  "josh": "Josh Stammer, M.D.", 
  "christine":"Christine Collins, M.D.", 
  "abraham": "Abraham Brown, M.D."
};


/**
 * A class representing a user in a chat with a bot
 */
class User {
  constructor(manager, postOutput = console.log, chatid = undefined) {
    this.manager = manager;
    this.nextIntent;
    this.date;
    this.time;
    this.datetime;
    this.chatid = chatid;
    if (postOutput === "tg") {
      this.postOutput = async function(text) {
        await this.constructor.bot.sendMessage(this.chatid, text);
      }
    } else {
      this.postOutput = console.log;
    } 
  }

  /**
   * Give additional behavior to intents:
   * @param {Object} input - an object, which is the output of NlpManager's process method
   * @returns {Object} - the same object as in the input but with possibly changed attributes such as "answer"
   */
  async onIntent(input) {
    const output = input;

    // Check if output has to be a logical continuation of the prior conversation's flow.
    // If it is, change the classified intent to the predefined one.
    if (this.nextIntent) {
      output.intent = this.nextIntent;
      this.nextIntent = undefined;
    }
    
       // Check if the user want to book something
    if (output.intent === 'user.book') {
      
      //...
      
      // If the user's intent is to talk to a human operator, redirect the chat
    } else if (output.intent === "user.redirect") {
      output.answer = "Redirecting to a human manager";

      // Imitate passing the chat to a human operator
      this.postOutput("## Passing the chat over to a human manager");

    } else if (output.intent === "None") {
      output.answer = "Sorry, I didn't quite get you. Could your paraphrase it?"
    }
    return output;
  }
}

The most important method here is onIntent, which will give additional behavior to some intents. For example, the intent β€œuser.book” will not only respond to the user, but also send a request to the API. onIntent will also be used to create a logical flow in a conversation. The instance variable nextIntent will be responsible for that. For instance, if our user inputs something like β€œI want to see a doctor”, the bot should ask the user to specify the doctor and time, and expect the next message to be interpreted only as the same β€œuser.book” intent. That's why we'll explicitly save and later use this info about the next intent in the variable.

Here're the inner workings of our booking process:

  // ...      

  // Check if the user want to book something
  if (output.intent === 'user.book') {

    if (output.entities) {
      // Go throught all found entities and add the important ones to instance variables
      for (let i = 0; i < output.entities.length; i++) {
        switch(output.entities[i].entity) {
          case "doctor":
            this.doctor = output.entities[i].option;
            break;
          case "date":
            let reservationDate = output.entities[i].sourceText;
            reservationDate = this.constructor.processDate(reservationDate);
            this.date = reservationDate;
            break;
          case "datetime":
            let reservationDatetime = output.entities[i].sourceText;
            reservationDatetime = this.constructor.processDate(reservationDatetime);
            this.datetime = reservationDatetime;
            break;
          case "time":
            let reservationTime = output.entities[i].sourceText;
            reservationTime = this.constructor.processDate(reservationTime, true);
            this.time = reservationTime;
            break;
        }
      }
    }

    // All the required data to book is present => process the reservation
    if (this.doctor && (this.datetime || (this.date && this.time))) {

      // If the user gave date & time separately, join them. If not, use the datetime
      let finalDatetime;
      if (this.datetime) {
        finalDatetime = this.datetime;
      } else {
        finalDatetime = this.date.advance(this.time);
      }

      // If the passed date or time are past
      if (finalDatetime < Date.now()) {

        output.answer = "Sorry, you've provided an unavailable time or date. Please repeat again";
        this.date = this.time = this.datetime = undefined;
        this.nextIntent = "user.book";

      } else {

        // Imitate sending the booking info to an API
        this.postOutput("## The request is sent to API");
        output.answer = `Your reservation with ${availableDoctors[this.doctor]} was made. `
                        + `Time: ${finalDatetime.toString()}. Thanks for working with us!`;

        // Rewriting the variables to give the user the ability to book more than once
        this.doctor = undefined;
        this.date = this.datetime = this.time = undefined;

      }

    // Not all data required to make a reservation is present => Iterate until it is
    // The iteration is done by means of the variable "nextIntent", which will
    // explicitly tell that the next message's intent also refers to booking (or other intents)
    } else {
      this.nextIntent = "user.book";
      output.answer = "Sorry, you have to specify";

      // The user hasn't provided the name of the doctor => Ask them to choose
      // out of the available ones
      if (!(this.doctor)) {
        var outputString = "Please choose the doctor out of the available:\n";
        for (let i = 0; i < Object.keys(availableDoctors).length; i++) {
          outputString += availableDoctors[Object.keys(availableDoctors)[i]] + "\n";
        }
        output.answer = outputString;
        return output;
      }
      // The user hasn't given the date or time. Ask them about it, while 
      // staying on "user.book" intent.
      if (!(this.date)) {
        output.answer = "Please enter the date";
        this.nextIntent = "user.book";
        return output;
      }
      if (!(this.time)) {
        output.answer = "Please enter the time";
        this.nextIntent = "user.book";
        return output;
      }
    }

  // ...

Let's go through it step-by-step:

  • Lines 6-30 are extracting the doctor's name, the date & the time of the reservation (if they are present) and saving them in the class instance.

  • Lines 33-61 explain what to do in case all the necessary data (i.e. doctor's name, date, and time) are present. If the date and time the user wants are past, the user will be asked to make another choice. In case everything’s fine, the user gets a success message.

  • The lines 66-90 define what to do in case not all information is available. Firstly, the user will be given a list of doctors to choose from. Then they will be asked to provide the date, and lastly β€” the time of the reservation.

The last thing we haven't spoken about is the processDate method, which will just parse a natural language string and extract either the date or the time offset from the beginning of the day:

  // ...

  static processDate(textDate, isTime = false) {
    
    // Do the actual parsing
    let date = Date.create(textDate);

    // Get the time offset in case required
    if (isTime) {
      let dayStartTime = date.clone();

      dayStartTime.beginningOfDay();
      return date - dayStartTime;
    }
    return date;
  }
  // ...

Also, don't forget to export the object with doctors and the class itself at the end of the file:

export {
  availableDoctors,
  User
}

Now, let's rewrite our index.js using our freshly-made class:

import { NlpManager } from "node-nlp";
import { User } from "./user.js";
import TeleBot from "telebot";
import dotenv from "dotenv";
dotenv.config()

// Imitate a database with user info. Key: Telegram chat id. Value: User instance
const userDatabase = {};

const manager = new NlpManager({ languages: ['en'], forceNER: true });
const bot = new TeleBot(process.env.BOT_TOKEN);

// As the bot is the same for all User instances, set the bot as a class property
User.bot = bot;

// Load the model. Then connect to the Telegram bot
(async() => {
  // Change to these lines in case there are any changes in the corpus
  // manager.addCorpus("corpus.json");
  // await manager.train();
  // manager.save()
  manager.load();

  bot.on('text', async function(msg) {
    let response;
    let chatid = msg.chat.id;

    // If the user doesn't yet exist in out database, add them
    if (!(chatid in userDatabase)) {
      userDatabase[chatid] = new User(manager, "tg", chatid);
    }

    if (msg.text === "/start") {
      await msg.reply.text("Hello! My name's Booker. How can I help you?");

    } else {
      response = await manager.process('en', msg.text);
      response = await userDatabase[chatid].onIntent(response);
 
      await msg.reply.text(response.answer);
    }
  });

  bot.start();

})();

Here, you can see that we set up an object called userDatabase. While in a real-world application you might want to store the data about your users, in this demo project that's overkill, so we just use a JS object instead.

Also, since we've already trained and saved our NN, we just load it from our automatically created β€œmodel.nlp” file.

Lastly, after our NlpManager has processed the user input, we pass the result to our User's onIntent, which further modifies the answer and sends it to the chat.

Phew, seems like this is it. Let's try it out now.

If you need to look at the code for building a chatbot once again, feel free to take a couple of steps back.

You can create chatbots with the help of various services, including working with chatbot development companies, using chatbot platforms to build it yourself, employing pre-written codes for chatbot development, or learning how to make your own LLM.

To sum up, we'd like to say that building & integrating a chatbot is hard but definitely worth it β€” it can help you optimize customer support, communicate with your clients, and target your audience better.

You can access all of the source code along with detailed documentation in our GitHub repo.

If you need any help with the development or have any questions left, we'd be happy to help you!

Building Apps for EV, IoT, Fitness & Digital Health since 2017.

Need a Dev Team that gets things done?

Let's Talk

Stormotion client David Lesser, CEO from Numina

They were a delight to work with. And they delivered the product we wanted. Stormotion fostered an enjoyable work atmosphere and focused on delivering a bug-free solution.

David Lesser, CEO

Numina

Cyril Troitsky

Cyril Troitsky

React Native Wizard @ Stormotion

Proficient in writing anything from E-Charging / Fitness React Native apps or technical documentatio...

Cyril Troitsky

Cyril Troitsky

React Native Wizard @ Stormotion

Proficient in writing anything from E-Charging / Fitness React Native apps or technical documentatio...

Read also

Let's Build Something Great Together?

Drop us a message