sperea.es
Published on

Chatbot development with Javascript

Authors

Generally, many chatbots are built using Saas platforms. The reason is that their functionality is often very similar, and it can be complicated to implement them from scratch. Therefore, it seems reasonable that the distribution model of many chatbots is based on cloud-accessible services that only need to be parameterized.

However, there are occasions when you may be interested in developing your own chatbot without resorting to one of these services. One of the reasons is that we want to centralize the logic of our application in a single bot that communicates with various platforms, so that we can cover a wide range of channels to interact with our customers.

One day, I would like to show you how to design one of these chatbots in React or Vue.js using web sockets, but today I'll show you a much simpler way to get started in this field using JavaScript: BotKit.

What is BotKit?

BotKit is a JavaScript library and a set of tools and plugins (i.e., a framework) that provides bot developers with a platform-independent interface to build chatbots. This way, programmers can forget about certain technical details essential to focus on creating their chatbot's specific features and then use that functionality in different applications.

In essence, it is a powerful and straightforward kit for creating conversational user interfaces, with intuitive functions like hears(), ask(), and reply() that do exactly what they say. I believe it can't get any simpler than this.

BotKit, as we will see now, has a very flexible system that allows us to handle dialogues with scripts and transactional conversations involving questions, branching logic, and other dynamic behaviors. The only limit is your imagination and programming skills.

Installation

The installation, as shown on its website, is quite simple, so I won't go into many details. To get started and try it out, just run this:

    mkdir mybot
    cd mybot
    yo botkit

After a simple wizard (where you'll need to decide, among other things, under which platform your bot will work), you can run your project, which by default will listen on port 3000:


npm start

Chat with me: http://localhost:3000

or

Webhook endpoint online:  http://localhost:3000/api/messages

The Controller

The controller can be described as the "brain" of our bot. This controller serves as our interface to all BotKit functionalities, and even chat events must be attached to the controller. It's like chunks of code where we tell our bot: "when the user does this, you should do that."

Below are the functions that the controller has, but to delve deeper into them, you'll need to explore the official documentation, as BotKit's possibilities are greater than they appear at first glance.

  • addDep()
  • addDialog()
  • addPluginExtension()
  • afterDialog()
  • completeDep()
  • getConfig()
  • getLocalView()
  • handleTurn()
  • hears()
  • interrupts()
  • loadModule()
  • loadModules()
  • on()
  • publicFolder()
  • ready()
  • saveState()
  • shutdown()
  • spawn()
  • trigger()
  • usePlugin()

One advantage of this framework having this architecture is that we can develop the controller independently of the platform. This way, we can use BotKit to develop bots for the following platforms:

  1. A web chatbot created with React or Vue.js
  2. A bot for Slack
  3. A bot for Webex Teams
  4. A bot for Google Hangouts
  5. A bot for Facebook Messenger
  6. A bot for Twilio SMS

Each type of bot would require dedicating an article because the topic is quite extensive. But, in general, the use of the controller will be quite similar. Let's see how we can generally program a controller in BotKit to act as an endpoint for any platform's chat:

const { Botkit } = require('botkit')

const controller = new Botkit({
  webhook_uri: '/api/messages',
})

controller.hears('.*', 'message', async (bot, message) => {
  await bot.reply(message, 'I heard: ' + message.text)
})

controller.on('event', async (bot, message) => {
  await bot.reply(message, 'I received an event of type ' + message.type)
})

Once you understand this piece of code, you're ready to understand how your chatbot works. For now, let's break down different parts of this code.

Listening to Messages

BotKit has an event controller to manage interaction with the user. One of its functions is called hears(), which helps configure the bot to trigger functions when it "hears" something from the user.

Let's see it with an example, but we'll go into more detail later:

controller.hears('hola', 'message', async (bot, message) => {
  // do something!
  await bot.reply(message, 'Hola Mundo')
})

As you can see, it couldn't be simpler: when the controller hears that the user writes "hello," it responds with "Hello World." Now we have our Hello World!

Event Management

Hears() helps us respond to conversational events, but our bot can also respond to other types of events. For example: when a user joins the channel, when a button is clicked, or when a file is uploaded. The pattern for handling these events is very intuitive for anyone familiar with JavaScript.

controller.on('channel_join', async (bot, message) => {
  await bot.reply(message, 'Bienvenido a mi canal')
})

Once the user connects to our chatbot, the bot will receive a constant stream of events ranging from text messages, notifications, to changes in the user's presence (enter, leave the chat, disconnect, etc.). We can even create our own events.

As you may have guessed by now, we use controller.on() to respond to events.

Extending BotKit with Middleware

In addition to performing actions based on responses to a particular message or event, BotKit can also passively carry out actions on messages by intercepting them through middleware functions. Middleware functions can dynamically modify messages in real-time, add new fields, trigger alternative events, and modify the bot's behavior.

Here's a simple example where middleware intercepts sent and received messages to display their contents on the console:

// Log every message received
controller.middleware.receive.use(function (bot, message, next) {
  // log it
  console.log('RECEIVED: ', message)

  // modify the message
  message.logged = true

  // continue processing the message
  next()
})

// Log every message sent
controller.middleware.send.use(function (bot, message, next) {
  // log it
  console.log('SENT: ', message)

  // modify the message
  message.logged = true

  // continue processing the message
  next()
})

The middleware essentially has three endpoints:

  1. ingest: similar to receive, but it executes before the message is processed.
  2. receive
  3. send

This functionality is very important for integrating your bot on many platforms since it allows you, among other things, to create pipelines and processes that manage messages without worrying about their origin. It's crucial depending on the architecture you want to implement for your application.

For instance, imagine a message arriving in raw format, but you might need to transform it in real-time to adapt it to each platform where the bot is integrated. Some messages may need to be sent through web sockets, while others might be managed through a webhook. You may want your chat to be end-to-end encrypted or have different message treatments based on the type of user.

In these and many other situations, middleware will be an essential tool.

Advanced Control of Conversations

BotKit has more advanced functions, including BotKitConversation, which allows you to create interfaces based on dialogues, buttons, options, etc.

Dialogues are created using functions like convo.ask() and convo.say(), and dynamic actions can be implemented using a binding system (convo.before(), convo.after(), and convo.onChange()) that provides conversation context and a bot worker at key points where code needs to be executed.

const { BotkitConversation } = require('botkit')

// define the conversation
const onboarding = new BotkitConversation('onboarding')

onboarding.say('Buenos días')
onboarding.ask(
  '¿Cómo puedo dirigirme a ti?',
  async (answer) => {
    // no hagas nada, sólo espera una respuesta
  },
  { key: 'name' }
)

// recolectar posibles valores. Cuidado con lo que respondes.
onboarding.ask(
  '¿Te gusta la tortilla con cebolla o sin cebolla?',
  [
    {
      pattern: 'con cebolla',
      handler: async function (answer, convo, bot) {
        await convo.gotoThread('me_gusta_con_cebolla')
      },
    },
    {
      pattern: 'sin cebolla',
      handler: async function (answer, convo, bot) {
        await convo.gotoThread('me_gusta_sin_cebolla')
      },
    },
  ],
  { key: 'tortilla' }
)

onboarding.addMessage('Me representas', 'me_gusta_con_cebollas')

onboarding.addMessage('Eso no es tortilla ni es nada', 'me_gusta_sin_cebolla')

onboarding.after(async (results, bot) => {
  const name = results.name
})

controller.addDialog(onboarding)

controller.hears(['hello'], 'message', async (bot, message) => {
  bot.beginDialog('onboarding')
})

Well, with this brief introduction, I hope you're now eager to experiment. For now, start absorbing this information, and in subsequent articles, I'll try to explain how to go further until you develop a complete chatbot."

Testing our Chatbot

Develop using TDD

Test-Driven Development (TDD) is especially useful in this type of software since chatbots usually require applying very short and rapid iterations of refactoring. Otherwise, as we add or remove parts of our conversation flow, we might end up with a lot of disconnected, useless, and "spaghetti" code.

We need to have tight control over the code to avoid writing more than what is actually needed, and above all, we must be able to detect bugs every time we change something in the conversation flow, as a small change can often affect other parts of the program.

It seems quite reasonable, then, to write unit tests for our Chatbot. So, let's see how to do it:

Option 1: Botkit-Mock

One of the first problems we may encounter with BotKit is that it depends too much on adapters with other applications (Slack, Facebook, MS Teams, etc.). That's why Botkit-Mock exists, which is an extension of Botkit that aims to provide an interface to accept user messages through .usersInput.

Unfortunately, the library is too new, and at the time of writing this article, we can only use Botkit-Mock with an adapter for Slack. So it will only be useful if you're building a bot for Slack.

To use it, we just need to install it in our project:

npm install --save botkit-mock

And include it in our project:

const { BotMock } = require('botkit-mock');

const fileBeingTested = require("./indexController")

Aquí tendrías un ejemplo de un test:


    'use strict';
    const assert = require('assert');
    const
    {
      BotMock,
      SlackApiMock
    } = require('../../../lib');
    const
    {
      SlackAdapter,
      SlackMessageTypeMiddleware,
      SlackEventMiddleware
    } = require('botbuilder-adapter-slack');
    const fileBeingTested = require(
      './dialog');
    describe('create dialog in a thread',
        () =>
        {
          const initController =
            () =>
            {
              const adapter =
                new SlackAdapter(
                {
                  clientSigningSecret: "some secret",
                  botToken: "some token",
                  debug: true,
                });
              adapter.use(
                new SlackEventMiddleware()
              );
              adapter.use(
                new SlackMessageTypeMiddleware()
              );
              this.controller =
                new BotMock(
                {
                  adapter: adapter,
                });
              SlackApiMock
                .bindMockApi(
                  this
                  .controller
                );
              fileBeingTested(this
                .controller);
            };
          beforeEach(() =>
          {
            this.userInfo = {
              slackId: 'user123',
              channel: 'channel123',
            };
          });
          describe('create_service',
            () =>
            {
              beforeEach(
                () =>
                {
                  initController
                    ();
                });
              it(`should reply in a correct sequence through message`,
                async() =>
                {
                  await this
                    .controller
                    .usersInput(
                      [
                      {
                        type: 'message',
                        user: this
                          .userInfo
                          .slackId, //user required for each direct message
                        channel: this.userInfo.channel, // user channel required for direct
                        message
                        messages: [
                        {
                          text: 'create_dialog_service',
                          isAssertion: true
                        }]
                      }]);
                  assert.strictEqual(this.controller.detailed_answers[this.userInfo.channel][0].text, `Howdy!`);
                });
            });

Option 2: TestMyBot

Given the limitations of the previous option, let's consider another one. TestMyBot is a test automation framework for chatbots. It is tool-agnostic regarding the tools involved in your development, and it is also free and open-source.

Capture and replay tools will record your test cases and run them against the implementation of your chatbot automatically, again and again. It is designed to be used in your delivery pipeline.

Let's see how to install TestMyBot and use it along with the Jasmine library (a framework for test development):

    npm install testmybot --save-dev
    npm install jasmine --save-dev
    ./node\_modules/.bin/jasmine init/code>

Add a file named spec/testmybot.spec.js with this content:

const bot = require('testmybot')
const botHelper = require('testmybot/helper/jasmine')

botHelper.setupJasmineTestSuite(60000)

Also, add a file named testmybot.json to your project folder:

    {
      "containermode": "local"
    }

The jasmine.js file is responsible for connecting your chatbot code with TestMyBot code. It generates a conversation and an XML report.

TestMyBot comes with built-in helpers for Jasmine and Mocha, but it can also be used with other libraries. It can even be used with chatbot projects written in other programming languages.

In general, our test cases will be conversations that the chatbot should be able to handle. The conversation transcription should be executed automatically, and any difference compared to the transcription should be reported as an error.

These test cases generally constitute a set of regression tests and ensure that, before deploying after any change, the chatbot still works correctly after it was changed or connected with other software.

Therefore, you should write tests that ensure any change in your chatbot won't break its conversation flows. Obviously, this implies that you will have to create tests for all possible conversation flows.

TestMyBot IDE To interact with our ChatBot, we have a tool called TestMyBot IDE, which provides a browser interface to record and organize our test cases and interact with it.

Moreover, the conversation will be saved in a text file that will later be used to verify that everything went as expected. Of course, it also has capture and replay tools that record your test cases and can be executed against the Chatbot's implementation at any time.

Command Line Interface If you don't like the graphical environment, TestMyBot includes a command-line interface to interact with your chatbot. It's very useful in server environments.

Let's see how it works

    npm install testmybot --save-dev
    npm install testmybot-fbmock --save-dev
    npm install jasmine --save-dev
    ./node\_modules/.bin/jasmine init

Add a testmybot.json file to your project folder. Also, a basic configuration is needed (the following one is for Docker):

    {
      "docker": {
        "container": {
          "testmybot-fbmock": {
            "env": {
              "TESTMYBOT\_BOTKIT\_WEBHOOKPORT": 3000,
              "TESTMYBOT\_BOTKIT\_WEBHOOKPATH": "webhook"
            }
          }
        }
      }
    }

Imagine a very simple chatbot that is programmed to respond "World" when you say "Hello" to it. The test case developed with Jasmine (spec/testmybot.spec.js) for this conversation flow would be as follows:

        describe('Hello World Bot', function() {
          beforeEach(function() {
            this.bot = require('testmybot').setup();
          });
          it('di hola', function() {
           expect(this.bot.hears('Hola').says().text())).toMatch(/mundo/);
          });
        });

Don't forget to include the script in your package.json.

    "scripts": {
      "start\_testmybot": "node index.js",
    },

As you can see, it's an object that sends text to your chatbot (it could be another type of content) and receives what your bot responds.

And then:

    ./node\_modules/.bin/jasmine init