Building bots with Redux

Overview

A key part of what makes the Azure Bot Service so great is it’s flexibility, which we’ve been emphasizing from the very beginning. When we’ve talked about flexibility in the past, we’ve typically referred to the Azure Bot Service’s ability to seamlessly connect your bots to multiple channels at once, and the option to easily add cognitive services such as LUIS or QnAMaker to create rich conversational experiences. In this post, to explore how flexible the Azure Bot Service can be, we’ll be venturing into relatively uncharted territory – we are going to build a bot and attempt to use as little of the bot builder SDK as possible, by leveraging another library to handle our bot’s dialog and user interaction.

Redux is a popular open source library which acts as a state container for JavaScript applications. It is currently perhaps most commonly used along side Facebook’s React.js library for building user interfaces. In a way, much of what a chat bot does is performing some task to process information – contextual conversation data, and user data. For this reason we thought that redux would make a great tool to experiment with, and we will showcase this by re-creating an existing bot.

This sample from the BotBuilder-Samples repo prompts the user for their name, and simply performs a bing search within a specified city.

Let’s get to it!

Prerequisites

First we need to provide some (hopefully familiar) boilerplate code. For  new bot developers, this is the only template code that we need to use – a connection client provisioned from the the bot builder SDK, and a restify server.

const builder = require('botbuilder');
const restify = require('restify');

const inMemoryStorage = new builder.MemoryBotStorage();
const connector = new builder.ChatConnector({
  appId: "Your-Microsoft-App-ID",
  appPassword: "Your-Microsoft-App-Password"
})

const bot = new builder.UniversalBot(connector).set('storage', inMemoryStorage); // Register in memory storage

const server = restify.createServer();

server.listen(process.env.port || process.env.PORT || 3978, function () {
  console.log(`${ server.name } listening to ${ server.url }`);
});

server.post('/api/messages', connector.listen());

// root dialog 
bot.dialog('/', new builder.SimpleDialog((session, result) => {
    // TODO: add some redux
}));

This is all of the code which we’ll be using from the bot builder SDK, everything we are going to cover below is going to use redux. Redux developers should feel right at home, and our goal today is to demonstrate that you can author bot conversational flows on the Azure Bot Service using many different technologies/libraries, developers are not restricted to what is provided by the bot builder SDK.

The next parts of the article are more conceptual, and we’ll break everything into three main steps:

  1. Provide a high level overview of redux concepts.
  2. Explain how those concepts from redux are applied to our bot scenario
  3. Introduce redux-saga, which is applied as redux milddleware to actually model the bot’s conversational flow.
  4. Describe how the data flow works

Redux Concepts

Redux defines three concepts which we’ll introduce briefly, and explain how each relates to our bot:

State in the store is updated by Actions, which are plain JavaScript objects which define information for what is happening. Actions are entirely up for the developer to define, and are best used as accurate descriptions for things your application actually needs to do. For example –

const RECEIVE_MESSAGE = 'RECEIVE_MESSAGE';

// Example of an Action
{
  type: RECEIVE_MESSAGE,
  payload: { attachments, result, text }
};

Reducers define how the application’s state will actually be updated after the action has been dispatched to the store. Reducers are pure functions which take some state and action, and return a new state:

 // Reducers are pure functions

(state, action) => newState

Application state is stored globally in what is defined as the Store. For this sample, we created a separate file called loadStore.js so that we can pass in a session object from the bot conversation.

// loadStore.js
const { applyMiddleware, createStore } = require('redux');

module.exports = function loadStore(session){
   const store = createStore(
     // apply middleware, 
   );

   store.subscribe(() => {
      // save store to conversationData
      session.conversationData = store.getState();
      session.save();
   });
 
   return store; 
};

You can check out the redux documentation links above to learn more about actions, reducers and the store. Back to our sample bot, in app.js, we can set up the root dialog to take incoming messages from a user, and dispatch the message as an action to the store.

// Redux
const loadStore = require('./redux/loadStore');
const DialogActions = require('./redux/dialogActions');


bot.dialog('/', new builder.SimpleDialog((session, result) => {
  const store = loadStore(session);
  const { attachments, text } = session.message || {};

  if (attachments || result || text) {
    store.dispatch(DialogActions.receiveMessage(text, attachments, result));
  }
}));

Above, incoming messages to the root dialog are dispatched to the store via a receiveMessage action.

Applying Redux to our Bot

Actions

Actions – it is key to note that actions only signify that something has happened. Actions must include the type property, which can freely be defined to describe the actions for the application. For example, in our bot scenario, we define the actions intuitively to match what our bot behavior should be – it should be able to receive a message, send a message, and so forth. 

// Actions

const SET_CITY = 'SET_CITY';
const SET_USERNAME = 'SET_USERNAME';
const RECEIVE_MESSAGE = 'DIALOG/RECEIVE_MESSAGE';
const SEND_MESSAGE = 'DIALOG/SEND_MESSAGE';

function setCity(city) {
  return { type: SET_CITY, payload: { city } };
}

function setUsername(username) {
  return { type: SET_USERNAME, payload: { username } };
}

function sendMessage(text, attachments) {
  return {
    type: SEND_MESSAGE,
    payload: { attachments, text }
  };
}

function receiveMessage(text, attachments, result) {
  // plain js object
  return {
    type: RECEIVE_MESSAGE,
    payload: { attachments, result, text }
  };
}

module.exports = {
  SET_CITY,
  SET_USERNAME,
  SEND_MESSAGE,
  RECEIVE_MESSAGE,
  receiveMessage,
  sendMessage,
  setCity,
  setUsername
}

You’ll notice that actions are simply plain JavaScript payloads which send information from the bot to the store.

 Reducer

Reducers define how an application’s state should change in response after an action is sent to the store.

Reducers accept two arguments –

1) current state

2) the action.

Below, conversationReducer is used to update a username, and the city to search by.

const { SET_CITY, SET_USERNAME } = require('./conversationActions');

const DEFAULT_STATE = {
  city: null,
  username: null
};

function conversationReducer(state = DEFAULT_STATE, action) {
  switch (action.type) {

  case SET_CITY:
    state = { ...state, city: action.payload.city };
    break;

  case SET_USERNAME:
    state = { ...state, username: action.payload.username };
    break;
  }

  return state;
}

module.exports = conversationReducer;

This allows the action itself to remain decoupled from the actual state change to the bot.

Store

Recall that we created a custom module called loadStore, so that we can pass a session object from the bot’s conversation. So, we’ve modeled how is contained and changed using redux, but so far we still haven’t created anything functional for the bot to actually do for the user. Shouldn’t the bot also be able perform send messages back to the user as well? Recall that in our bot scenario, we’re trying to create a bot which will set a user’s username, and create a search query based on a city location.

Redux itself is here only to manage state changes for the bot, and not actually perform actions related to those state changes. In order to perform tasks like sending a message back to the user, we’ll need to add something extra.

Redux-Sagas

Redux-Saga is a library which we can leverage to process the actual conversations for the bot. According to the documentation,

redux-saga is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage, more efficient to execute, simple to test, and better at handling failures.

This layer acts as redux middleware, and will be used to model the bot’s conversation flow, and acts as a layer of application logic between the request and the response in the framework.

For bots we can create a general state container with redux, and author the conversational flow entirely separately using redux-saga. With redux we can model state using only the data we need, and redux-saga acts as the middleware which is able to perform outside operations such as sending a response message back to the user. Redux-Saga implements sagas using generator functions, have full access to redux state, and can also dispatch actions as well.

Below is an example of a saga used to control the dialog for our scenario –

const { put, select, takeEvery } = require('redux-saga/effects');
const { reset, setCity, setUsername } = require('../conversationActions');
const { promptText, RECEIVE_MESSAGE, sendMessage } = require('../dialogActions');

module.exports = function* (session) {
    //  creates a saga on each action dispatched to the store based on the pattern - i.e RECEIVE_MESSAGE
   yield takeEvery(RECEIVE_MESSAGE, function* (action) {
      const { text } = action.payload;
      const changeCityMatch = /^change city to (.*)/i.exec(text);
      const currentCityMatch = /^current city/i.exec(text);
      let { city, username } = yield select(); 
      
      if(!city){
         city = 'Seattle';
         yield put(setCity(city));
         yield put(sendMessage(`Welcome to the Search City bot. I\'m currently configured to search for things in ${ city }`));
         yield put(promptText('Before get started, please tell me your name?'));
       } 
      ....
   });
}

Once sagas are defined, we need to register the sagas to the store as middleware. It’s important to note that sagas trigger between dispatching an action, and before reducers. 

const { applyMiddleware, createStore } = require('redux');
const { default: createSagaMiddleware } = require('redux-saga');

const createDefaultSaga = require('./sagas/default');
const createDialogSagas = require('./sagas/dialog');

module.exports = function loadStore(session) {
  const saga = createSagaMiddleware();

  ...
      
   applyMiddleware(
       saga,
       store => next => action => {
           session.send({
               type: 'event',
               name: 'action',
               value: action
           });
        return next(action); 
       }
     )
   );

    saga.run(function* () {
      yield* createDialogSagas(session);
      yield* createDefaultSaga(session);
    });

  return store;
};

How the data flows

  1. From the root dialog, the bot dispatches an action using store.dispatch(action). Actions are plain JavaScript objects which describe what has happened. For a bot, you can describe it like, “user sent a message”, “bot responded with a message”, etc.
  2. The bot uses Redux-Saga middleware to perform ‘side effects’ such as responding to the user, leaving conversational flow decoupled from state.
  3. Redux store calls the reducer function provided by the action. The reducer itself is a pure function which only updates the state, based on the action.
  4. State is updated in the global store

Running the bot

Click here for the full sample on GitHub. We’ll verify that our new bot using redux has the same feature set as the original sample bot which only uses the BotBuilder Node.js SDK.

 

Great! Our bot behaves just like the original sample. We’ve verified that we can use redux to create author bot logic, without using too much of the BotBuilder SDK. Taking this a step even further, redux store is a global object. We also know that it’s a popular library to use in conjunction with React.js to create UI.

Rendering Bot State

Since we are using Redux to store global state, we thought it’d be interesting to see how far we can take this sample, and render the contents of the store to the web app in simple HTML. So, we’ve also included a simple custom web app with it’s own web chat instance. This is purely optional, and the sample bot will still run if you choose to just use the emulator.

public/index.html 

Since this custom web app is running on its own instance of web chat over the emulator, we created a new bot registration on Azure to use the DirectLine channel. We also need to use ngrok to allow port forwarding to allow the Azure Bot Service to communicate back to our web app.

Running ngrok on localhost:3978, copy the URL provided, and apply it to the messaging endpoint of the bot channels registration config as show below:

Note: make sure the URL ends in /api/messages

After the ngrok URL is set as the messaging endpoint, we are ready to start the bot. From your CLI, simply enter npm start:

npm start

Note: The package.json file included defines the scripts to run and defaults to localhost:3000

The simple web app provided gets the required credentials using URLSearchParams –

const params = new URLSearchParams(location.search);

window['botchatDebug'] = params.get('debug') && params.get('debug') === 'true';

const botConnection = new BotChat.DirectLine({
     domain: params.get('domain'),
     secret: params.get('s'),
     token: params.get('t'),
     webSocket: params.get('webSocket') && params.get('webSocket') === 'true' // defaults to true
});

So, we need to add the channel registration’s direct line secret directly in the URL before the application will run in the browser.

You should now see a web chat instance in the browser – along with a simple render of the objects being saved in the redux store.

Notice that as the state changes, we can visually see changes to the UI render! Because of this global state store, you can create interactions beyond the scope of the bot itself. Here we are simply updating the render to an HTML page. We’ve demonstrated that it’s possible to interact with a page using a bot, then just imagine the possibilities on a fully featured website, or enabling speech!

Adding a LUIS Recognizer

The sample bot we’ve provided only includes recognition for static regular expressions, so what if we wanted to add some natural language?

const LuisModel = `https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/${ LuisAppID }?subscription-key=${ LuisKey }`;
const recognizer = new builder.LuisRecognizer(LuisModel);

bot.recognizer(recognizer);

const loadStore = require('./redux/loadStore');
const DialogActions = require('./redux/dialogActions');

bot.dialog('/', [(session, args, next) => {
  const { attachments, text } = session.message;
  const store = loadStore(session);

  store.dispatch(DialogActions.receiveMessage(text, attachments));
  store.dispatch(DialogActions.sendMessage('Sorry, I don\'t know what you say. You can say: add.'));
  store.dispatch(DialogActions.end());
}]);

// Intents provisioned by a LUIS application to be defined in an array, for example:
// const HOISTED_LUIS_INTENTS = ['Intent1', 'Intent2', 'SomeIntent', 'AnotherIntent'];

const HOISTED_LUIS_INTENTS = ['Add', 'Delete', 'Update', 'Edit'];

HOISTED_LUIS_INTENTS.forEach(name => {
  bot.dialog(`/${ name }`, [(session, args, next) => {
    const store = loadStore(session);
    if (args) {
      store.dispatch(DialogActions.receiveLuisIntent(args.intent));
    }
  }]).triggerAction({
    matches: name
  });
});

You can also very simply add a LUIS recognizer to this bot! In the code snippet above, we simply created a LUIS connection client and added the recognizer to the bot. Then, each LUIS intent is statically declared in an array. When a user sends a message to the bot, LUIS will return an intent, and that intent is dispatched as an action to redux store.

Summary

We hope that this inspires you to experiment with bots not only using Redux, but with any other technologies or libraries you can imagine. The goal of this post was to demonstrate how flexible the Azure Bot Service can be. It can not only deploy seamlessly across different channels, it also doesn’t restrict developers to using the BotBuilder SDK to create their conversational flows. There are many, many other existing frameworks and libraries out there, most of which we have yet to test out.

You can find the full sample on GitHub here.

Happy Building!

William Wong and Matthew Shim from the Azure Bot Service Team

References

Official Redux Repo on GitHub

Redux-Saga on GitHub

ngrok