© Abhishek Singh, Karthik Ramasubramanian, Shrey Shivam  2019
A. Singh et al.Building an Enterprise Chatbothttps://doi.org/10.1007/978-1-4842-5034-1_6

6. A Novel In-House Implementation of a Chatbot Framework

Abhishek Singh1 , Karthik Ramasubramanian1 and Shrey Shivam2
(1)
New Delhi, Delhi, India
(2)
Donegal, Donegal, Ireland
 

In previous chapters, we explained intents and different ways of classifying intents using natural language techniques. We also discussed the various data sources that are available in designing an enterprise chatbot. There are many chatbot builder platforms and frameworks available in the market that can be used to build chatbots. These frameworks abstract much complex functionality and provide components that are reusable, extendable, and scalable.

Designing an enterprise chatbot without using a framework has the following benefits:
  • Provides better security and control

  • Data protection from third-party vendors

  • Minimizes operational cost

  • In-depth analytics

  • Flexible design of architecture

  • Change control management

  • Interoperability

  • Easy and quick integration with enterprise-wide available services and frameworks

  • Integration with messenger platforms

  • Integration with custom machine learning models

  • Flexibility to customize with changes in the organization

In this chapter, we will discuss and implement a custom-built chatbot called IRIS (Intent Recognition and Information Service) . We will explain the implementation concepts of the understanding developed in previous chapters. We will talk about designing and implementing state machines, transitions from one state to another in a conversational chatbot, and how they are critical to maintaining the context of user utterances as well as how to make a chatbot mimic human conversation with short-term memory and long-term memory.

In IRIS, the backbone engine of the chatbot is written in Java, and an integration module connects IRIS with different messenger platforms such as Facebook Messenger. The integration module is written in NodeJS, which is discussed in the next chapter.

Introduction to IRIS

We developed IRIS as an open source chatbot framework to provide a novice level of understanding and implementation of a chatbot from scratch. IRIS provides the ability to use our templates of machine learning-based extraction of information from users’ utterances, such as by using named-entity recognition (NER). It provides customized enhancements such as custom intent matching implementation and conversational state management and many other features.

The design is inspired by our collective experience and exploration of how other popular frameworks such as Amazon’s Echo natural language understanding model, Alexa Skills, RASA, Mutters, Dialogflow, and Microsoft Bot Builder are designed and implemented. IRIS derives many methods and implmentation from Mutters, an open source Java-based framework for building bot brains, and reuses some of its design and code concepts to create a simple and modified backend code base. Platforms like Mutters provide a lot of out-of-box features, support, and easy integration. Apart from our custom chatbot framework, we will discuss widely popular platforms and frameworks, and how they work, in the next chapter.

Intents, Slots, and Matchers

In the previous chapter, we showed that intents are an outcome of the behavior and focus of the user’s utterance. In this chapter, we will describe various components of an intent and discuss the implementation approach of creating and classifying intents. The following components define an intent:
  • A name

  • Sample utterances

  • Slots (entities)

  • A slot matcher

Each intent can have zero or more slots, which are used to extract entities from user utterances. For example, if a chatbot helps us discover restaurants nearby, one of the intents could be defined as the following:

Intent name

Restaurant search

Sample utterances

Looking for restaurants around me

Restaurants nearby

Best restaurants near me

Good continental restaurants nearby

Best Chinese restaurants

Slots/Entity

Cuisine

Slot matcher

Custom Entity Match Model

Figure 6-1 explains the meaning of intent, slot, and utterance.
../images/478492_1_En_6_Chapter/478492_1_En_6_Fig1_HTML.jpg
Figure 6-1

The meanings of intent, slot, and utterance

Now, we’ll procedurally go through all the steps involved in creating intent, slot, and matcher classes in Java.

In our new Java project, we will create a package called com.iris.bot.intent in which we will define classes required for intent creation and classification.

Intent Class

We define a Java class called Intent with a name variable to store the intent name. Intent has slots that contain a list of 0 or more slots defined for this particular intent. We have getters and setters of the name and slots.
public class Intent {
      /** The name of the intent. */
      protected String name;
      /** The slots for the intent.
       * There could be 0 or more slots defined for each intent.
       * Slots contain a list of Slot and methods to add and get Slot  */
      protected Slots slots = new Slots();
      /**
       Constructor with the name as a parameter.
       It sets the Intent name at the time of intent creation.
       */
      public Intent(String name) {
            this.name = name;
      }
      /**
       * Returns the name of the intent.
       */
      public String getName() {
            return name;
      }
      /**
       * Adds a slot to the intent.
       */
      public void addSlot(Slot slot) {
            slots.add(slot);
      }
      /**
       * Returns the slots for the intent.
       */
      public Collection<Slot> getSlots() {
            return Collections.unmodifiableCollection(slots.getSlots());
      }
}

Now that we have defined Intent, we need to define the IntentMatcherService class.

IntentMatcherService Class

This service takes user utterances and responds with the matched intent. As explained in the previous chapter, there are multiple ways to classify intents. In this example, we have a separate intent classification service that classifies user utterances into one of the user-defined intents with a certain probability or score (refer to Chapter 5 for more details on intent classification).
public class IntentMatcherService {
      /** A map of possible intent names and intents that are defined in the Iris Configuration */
      private HashMap<String, Intent> intents = new HashMap<String, Intent>();
      /** The slot matcher method to use for named entity recognition. */
      private CustomSlotMatcher slotMatcher;
      /** Intent Matcher Service constructor that sets slot matcher */
      public IntentMatcherService(CustomSlotMatcher slotMatcher) {
            this.slotMatcher = slotMatcher;
      }
      /*
       * RestTemplate is a synchronous Java client to perform HTTP requests, exposing a simple template method API over underlying HTTP client libraries. The RestTemplate offers templates for common scenarios by HTTP method, in addition to the generalized exchange and execute methods that support less frequent cases.
       */
      protected RestTemplate restTemplate = new RestTemplate();
      /** This method takes a user utterance and session as an input, obtains matched intent from an intent classification service, performs named entity recognition on slots defined for the matched intent, and sets the matched intent into the user session. The session is a server-side storage mechanism that stores a user's interaction and resets the information or persists based on the interaction duration and the type of information.
       */
      public MatchedIntent match(String utterance, Session session) {
            // getIntent method returns the matched intent.
            Intent matchedIntent = getIntent(utterance);
            /*
             * We define slots associated with each Intent in the Iris Configuration class. Each of these slots has a matching method defined to describe how entities are to be matched. Depending on the entity and implementation, various NER models can be used to recognize entities. This method returns a map of the slot and matched slot object. The slot contains a slot name and a matching method, and MatchedSlot contains slot that was matched, the value that was used to match on, and the value that was matched.
             */
            HashMap<Slot, MatchedSlot> matchedSlots = slotMatcher.match(session, matchedIntent, utterance);
            /*
             * Once we get the matched intent, we set the value of the intent in session. We will discuss session under the IRIS Memory topic.
             */
            session.setAttribute("currentIntentName", matchedIntent.getName());
            /*
             * Finally, an object with matched intent, matched slots, and the utterance against which the intents and slots were matched and returned.
             */
            return new MatchedIntent(matchedIntent, matchedSlots, utterance);
      }
}

In the code snippet above, Intent matchedIntent = getIntent(utterance) is the method that provides intent classification.

The getIntent Method of the IntentMatcherService class

As discussed, there are many ways in which this method can be implemented. It takes a user utterance as an input and returns an Intent that is classified with maximum probability by the intent engine. For now, let’s see how to define this method in a simple way:
/*
* getIntent method takes a user utterance and returns an object of type Intent. This is then used by the match method to match slots for that intent.
       */
      public Intent getIntent(String utterance) {
            /*
             * Intent Response is a plain Java object with three attributes - utterance, intent name, and probability returned by the intent service.
             */
            IntentResponse matchedIntent = new IntentResponse();
            /*
             * If the intent classification engine is not able to classify the utterance into some intent with some threshold or if the engine is unable to return a valid response, we fallback it to be a general query intent to be on the safe side.
             */
            String defaultIntentName = "generalQueryIntent";
            String matchedIntentName = null;
            /*
             * ObjectMapper provides functionality for reading and writing JSON, either to and from basic POJOs (Plain Old Java Objects), or to and from a general-purpose JSON Tree Model (JsonNode), as well as related functionality for performing conversions. ObjectMapper is a part of the com.fasterxml.jackson.databind package, which is a high-performance JSON processor for Java.
             */
            ObjectMapper mapper = new ObjectMapper();
            /*
             * There is specific enumeration that defines simple on/off features to set for ObjectMapper. ACCEPT_CASE_INSENSITIVE_PROPERTIES is a feature that allows for more forgiving deserialization of incoming JSON.
               FAIL_ON_UNKNOWN_PROPERTIES is a feature that determines whether encountering of unknown properties (ones that do not map to a property, and no "any setter" or handler can handle it) should fail (by throwing a JsonMappingException) or not.
             */
            mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            try {
                   matchedIntent = restTemplate.getForObject("http://localhost:8080" + "/intent/" + utterance, IntentResponse.class);
                   if(matchedIntent != null && matchedIntent.getIntent()!=null){
                         matchedIntentName = matchedIntent.getIntent();
                   }
                   else
// If matched intent is null, we consider the default intent to be the matched intent.
                         matchedIntentName = defaultIntentName;
            } catch (Exception e) {
// In case of an exception too, we consider default.
                   matchedIntentName = defaultIntentName;
            }
// Finally, we return the intent object with the matched intent name back to the match method of Intent Matcher Service.
            return intents.get(matchedIntentName);
      }

There are two essential things to be discussed in the getIntent method , and we cover them in the next sections.

Intent Classification Service

We are assuming here that there is an intent classification service running on localhost on port 8080 that accepts HTTP GET requests and returns a JSON response:
http://localhost:8080/intent/user-utterance
Here’s the JSON representation of a response:
{
"utterance": "i want a life insurance quote",
"intent": "QUOTE",
"probability": 89.5,
}

General Query Intent

Most chatbots today are based on general queries and look like an automated Q&A system. The reason for this is that most developers are unsure how to model the chatbot to be conversational. Also, they find it difficult to make the bot interactive.

A general query is never an explicit intent in a chatbot that is conversational and that mimics human conversation modeled as dialogs. Hence, when no intent is matched by the classification engine or if the match probability is not good enough for that utterance, we tend to classify it as general intent. We have seen that this approach is very efficient in practical situations. In another way, if the intent engine is not able to classify the utterance, the utterance could be a generic ask and not aimed for a specific action. We will show later how to use this intent for first looking for an answer in a FAQ repository and then later as a fallback, performing a general search to return a relevant response if possible.

Matched Intent Class

The last thing that we need to include in the com.iris.bot.intent package is a MatchedIntent class . It holds the intent that was matched, a map of slots that were matched against the defined slots for the intent, and the utterance against which they were matched.
public class MatchedIntent {
      /** The intent that was matched. */
      private Intent intent;
      /** Map of slots that were matched. */
      private HashMap<Slot, MatchedSlot> slotMatches;
      /** The utterance that was matched against. */
      private String utterance;
      /**
       * Constructor.
       *
       * @param intent
       *            The intent that was matched.
       * @param slotMatches
       *            The slots that were matched.
       * @param utterance
       *            The utterance that was matched against.
       */
      public MatchedIntent(Intent intent, HashMap<Slot, MatchedSlot> slotMatches, String utterance) {
            this.intent = intent;
            this.slotMatches = slotMatches;
            this.utterance = utterance;
      }
      /**
       * Returns the Intent that was matched.
       */
      public Intent getIntent() {
            return intent;
      }
      /**
       * Returns the slots that were matched.
       */
      public Map<Slot, MatchedSlot> getSlotMatches() {
            return Collections.unmodifiableMap(slotMatches);
      }
      /**
       * Returns the specified slot match if the slot was matched.
       *
       * @param slotName
       *            The name of the slot to return.
       * @return The slot match or null if the slot was not matched.
       */
      public MatchedSlot getSlotMatch(String slotName) {
            for (MatchedSlot match : slotMatches.values()) {
                   if (match.getSlot().getName().equalsIgnoreCase(slotName)) {
                         return match;
                   }
            }
            return null;
      }
      /**
       * Returns the utterance that was matched against.
       *
       * @return The utterance that was matched against.
       */
      public String getUtterance() {
            return utterance;
      }
}

Slot Class

We covered intent, intent matcher service, and matched intents so far. Designing classes of slots is similar to intents. We define slot-related classes in the com.iris.bot.slot package:
/*
 * Slot is defined as an abstract class. The concrete class of Slot implements a match method that contain the entity recognition logic.
   getName returns the slot name that is described in concrete slot classes.
 */
public abstract class Slot {
      public abstract MatchedSlot match(String utteranceToken);
      public abstract String getName();
}
With Slot defined, we create slots for the intent. Slots is an attribute specified in the Intent class and slot details are provided in the IRIS configuration.
/** The slots for the intent.
 * There could be 0 or more slots defined for each intent.
 * Slots contain a list of Slot and methods to add and get Slot  */
public class Slots {
/** The map of slots. */
      private HashMap<String, Slot> slots = new HashMap<String, Slot>();
      /**
       * Adds a slot to the map.
       */
      public void add(Slot slot) {
            slots.put(slot.getName().toLowerCase(), slot);
      }
      /**
       * Gets the specified slot from the map.
       */
      public Slot getSlot(String name) {
            return slots.get(name.toLowerCase());
      }
      /**
       * Returns the slots in the map.
       */
      public Collection<Slot> getSlots() {
            return Collections.unmodifiableCollection(slots.values());
      }
}
Similarly to MatchedIntent class, we create the MatchedSlot class to hold the details of the slots that were matched:
/*
 * MatchedSlot contains slot-related information such as the slot that was matched, the original value that was was used to match on, and the value that was matched.
 */
public class MatchedSlot {
      /** The slot that was matched. */
      private Slot slot;
      /** The original value that was used to match on. */
      private String originalValue;
      /** The value that was matched. */
      private Object matched value;
      public MatchedSlot(Slot slot, String originalValue, Object value) {
            this.slot = slot;
            this.originalValue = originalValue;
            this.setMatchedValue(value);
      }
      /**
       * Returns the slot that was matched.
       */
      public Slot getSlot() {
            return slot;
      }
      /** Returns the original value.
       */
      public String getOriginalValue() {
            return originalValue;
      }
      /*
       * Returns the matched value.
       */
      public Object getMatchedValue() {
            return matched value;
      }
      /*
       * Sets the matched value in the constructor. The method is declared as private as the value is only set in constructor.
       */
      private void setMatchedValue(Object matchedValue) {
            this.matchedValue = matchedValue;
      }
}
We defined Slot, Slots, and MatchedSlot so far under the com.iris.bot.slot package. Now let’s see how a custom slot matcher is defined. CustomSlotMatcher is invoked in IntentMatcherService to get the slot match information once an intent is obtained from the intent classification service:
/*
 * The CustomSlotMatcher class is used to iterate on all the slots for the matched intent and execute a match method of each of those slots to return all the matched slots. This class can be further customized and designed to have multiple types of slot matcher implementation.
 */
public class CustomSlotMatcher {
      /*
       * The match method takes session, intent, and user utterance as an input and returns a map of Slot and MatchedSlot details.
       * This method can further contain business logic depending upon the implementation.
       */
      public HashMap<Slot, MatchedSlot> match(Session session, Intent intent, String utterance) {
            HashMap<Slot, MatchedSlot> matchedSlots = new HashMap<Slot, MatchedSlot>();
// Iterate intent to get all slots defined for this matched intent.
            for (Slot slot : intent.getSlots()) {
/*
* Use case-specific business logic handling. askQuoteLastQuestion is a session variable, and its logic will be explained when we discuss state machines and conversation flow management.
 */
                   String slotCheck = String.valueOf(session.getAttribute("askQuoteLastQuestion"));
                   if (slot.getName().equalsIgnoreCase(slotCheck) || slotCheck.equalsIgnoreCase("null")) {
// The match method defined in each slot is executed, and MatchedSlot is returned.
                         MatchedSlot match = slot.match(utterance);
                         if (match != null && match.getMatchedValue() != "null") {
                               matchedSlots.put(slot, match);
                         }
                   }
            }
            return matchedSlots;
      }
}

We have so far covered how intent- and slot-related classes can be defined for IRIS. We briefly discussed how IRIS memory is managed by session attributes. Let’s go through this in some more detail.

IRIS Memory

A conversational chatbot needs to hold certain information to be able to closely mimic human-like responses. IRIS is designed to hold information in memory through sessions.

Long- and Short-Term Sessions

Session contains two types of attributes:
  • Long-term attributes

  • Short-term attributes

Long-Term Attributes

Certain entities such as name, date of birth, and gender of the user are information that does not change over time. Also, in the real world, we don’t expect our advisors and agents to ask these details every time we interact with them. In the current design of IRIS, what we have demonstrated is that not all attributes are reset after the user session. The long-term attributes that span sessions are supposed to be held in a fast, reliable, and persistent storage databases such as Redis. Redis is an in-memory database. In the code snippet that follows, we show this using a HashMap. Information in HashMaps are stored in JVM when the application is running and get cleared when the application goes down. Hence, even though they are long-term attributes, unless we persist them in a permanent storage like SQL databases, we can’t retrieve them again.

Short-Term Attributes

Unlike name and gender, certain attributes are limited to the scope of the user session. In most cases, the expectation is that the values will vary in each session. An example is a user providing a ZIP code when asking for an insurance agent nearby or providing a face amount for a life insurance eligibility quotation. Moreover, to manage the conversation flow, certain values such as current intent, state, and last question asked are stored as short-term attributes. Short-term attributes reset with each new session or if a session expires.

The Session Class

The Session class helps in conversation flow management by storing state- and intent-related information in attributes. It also helps in maintaining the information exchange between the user and the server by serving as a temporary storage layer. There is a reset method that reinitialize attributes when called. A session is created with a current timestamp and an empty attributes map.
public class Session {
      /*
       * We defined a session to be 30 minutes long and this is number should vary based on use case, and for how long do you want a session to be active.
       */
      public long expiryTimeinMilliSec = 30 * 60 * 1000l;
      private HashMap<String, Object> attributes = new HashMap<String, Object>();
      /*
       * Long-term attributes do not get reset when the session expires or when the reset method is called.
       */
      private HashMap<String, Object> longTermAttributes = new HashMap<String, Object>();
      // Time in milliseconds when the session was created. This is used to check whether the session is valid.
      private long timestamp;
      /*
       * A default session constructor is called and it assigns current time in milliseconds to the timestamp variable.
       */
      public Session() {
            this.timestamp = System.currentTimeMillis();
      }
      public void updateCurrentState(State currentState) {
            attributes.put("current_state", currentState);
      }
      public void updateCurrentIntent(String currentIntent) {
            attributes.put("current_intent", currentIntent);
      }
      /*
       * Checks if this is a valid session. Returns boolean.
       */
      public boolean isValid() {
            if (timestamp + expiryTimeinMilliSec < System.currentTimeMillis())
                   return false;
            return true;
      }
      /*
       * Returns a session attribute.
       */
      public Object getAttribute(String attribute) {
            return attributes.get(attribute);
      }
      /*
       * Sets a session attribute.
       */
      public void setAttribute(String key, Object object) {
            attributes.put(key, object);
      }
      /**
       * Removes the specified attribute from the session.
       */
      public void removeAttribute(String attributeName) {
            attributes.remove(attributeName.toLowerCase());
      }
      /**
       * Resets the session, removing all attributes. Long-term attributes
       * are not removed from the session.
       */
      public void reset() {
            attributes = new HashMap<String, Object>();
      }
}
We also need a helper class called SessionStorage to create sessions as well as maintain Session for each user. Slots matched in the match method are also saved to session for later use.
/*
 * Helper class which holds all user sessions and also provides method to get or create session.
 */
public class SessionStorage {
      // A map of user id and  sessions.
      HashMap<String, Session> userSession = new HashMap<String, Session>();
      /*
       * This method first checks if there is a session for this user (user ID). It also checks if the session is valid.
       *If there is no session for that user or if the session has expired, it will create a new session. Else it will return the active session.
       */
      public Session getOrCreateSession(String userId) {
            if (!userSession.containsKey(userId) || !userSession.get(userId).isValid()) {
                   Session session = new Session();
                   userSession.put(userId, session);
            }
            return userSession.get(userId);
      }
      /**
       * Gets a String value from the session (if it exists) or the slot (if a match exists).
       *
       * @param match
       *            The intent match.
       * @param session
       *            The session.
       * @param slotName
       *            The name of the slot.
       * @param defaultValue
       *            The default value if not a value found in the session or slot.
       * @return The string value.
       */
      public static String getStringFromSlotOrSession(MatchedIntent match, Session session, String slotName,
                   String defaultValue) {
            String sessionValue = (String) session.getAttribute(slotName);
            if (sessionValue != null) {
                   return sessionValue;
            }
            return getStringSlot(match, slotName, defaultValue);
      }
      /**
       * Gets a String based slot value from an intent match.
       *
       * @param match
       *            The intent match to get the slot value from.
       * @param slotName
       *            The name of the slot.
       * @param defaultValue
       *            The default value to use if no slot found.
       * @return The string value.
       */
      public static String getStringSlot(MatchedIntent match, String slotName, String defaultValue) {
            if (match.getSlotMatch(slotName) != null && match.getSlotMatch(slotName).getMatchedValue() != null) {
                   return (String) match.getSlotMatch(slotName).getMatchedValue();
            } else {
                   return defaultValue;
            }
      }
      /**
       * Saves all the matched slots for an IntentMatch into the session.
       *
       * @param match
       *            The intent match.
       * @param session
       *            The session.
       */
      public static void saveSlotsToSession(MatchedIntent match, Session session) {
            for (MatchedSlot matchedSlot : match.getSlotMatches().values()) {
                   session.setAttribute(matchedSlot.getSlot().getName(), matchedSlot.getMatchedValue());
            }
      }
}

So far we showed how to create intent and slot classes and the matchers, and we discussed IRIS long-term and short-term memory. We will now discuss an essential concept for chatbot: conversation management. In the next section, we will explain how conversations can be modeled as finite state machines and used in IRIS.

Dialogues as Finite State Machines

Typically, a simple Q&A-based chatbot or a FAQ-based chatbot is not capable of having a conversation. A conversational chatbot should support complex dialog flow between the user and the bot, and we aim to build a chatbot that can mimic human conversation as much as possible. Usually, chatbots are limited to a request-response based flow and are not driven as dialog or conversations.

Building a chatbot by conversational state management helps in transitioning from one state to another. As shown in Figure 6-2, a state machine reads a series of inputs and switches to another state once it receives an input required to perform that transition.
../images/478492_1_En_6_Chapter/478492_1_En_6_Fig2_HTML.jpg
Figure 6-2

Showing a finite state machine with states and transitions between states

We’ll explain this with a simple example. In Figure 6-3, we have a finite state representation of switching off and on a light bulb. There are two states: OFF and ON.

As in a finite state graph, you can only be in one state at a time. In the example of a light bulb, as shown in Figure 6-3, either it can be OFF or ON but not both at the same time. Also, to move from one state to another, a transition must take place. If the bulb is in the OFF state and we need to transition to ON state, we need to flip the switch up, which is an action/condition/prerequisite to transition to the ON state.
../images/478492_1_En_6_Chapter/478492_1_En_6_Fig3_HTML.jpg
Figure 6-3

State machine for a switch ON and OFF

A state machine has the following components:
  • States: Different states a bot can be in and transition to.

  • Initial State: This is the start state when the user first interacts with IRIS.

  • Transitions: The action(s) that should trigger a possible state change.

  • Shields: A prerequisite or condition to transition to a target state.

A state machine can be designed in multiple ways. It can be modeled as a graph, the conversation could be modeled as a script, or it could be implemented using a very naive approach like HashMaps and some classes we will discuss next.

We need to create a new package in our project called com.iris.bot.state to contain the base classes for the state machine.

State

Let’s first define State :
/*
 * State is an abstract class. Concrete State classes implement an execute method which is triggered when a transition to that state happens.
 */
public abstract class State {
      String name;
      public String toString() {
            return name;
      }
      public State(String stateName) {
            name = stateName;
      }
      public String getName() {
            return name;
      }
      /*
       * The execute method takes a session and matched intent as an argument. The action of the state is defined in this method.
       */
      public abstract String execute(MatchedIntent matchedIntent, Session session);
}

The transition from one state to another may sometimes require a validation condition. We’ll explain this with an example. If you are searching for a restaurant of your choice by interacting with a restaurant table booking chatbot, you can ask for cancellation only if you have booked a table at a restaurant. Otherwise, you cannot reach the state of cancellation.

Shields

To maintain any preconditions that may include business logic, we have Shield, which validates whether the transition to the desired state is possible or not.
/*
 * Shield is an interface. The class that implements Shield implements the validate method and returns true if the validation condition is met.
 * Otherwise false will be returned and transition to that state will not happen.
 */
public interface Shield {
      public boolean validate(MatchedIntent match, Session session);
}

Transition

It is the Transition class that holds Shield and the target state information. The transition is an elementary class with two member variables, toState and shield, and their getters and setters as defined here:
/*
 * Transition class holds target state and shield information
 */
public class Transition {
      private State toState;
      private Shield shield;
/*
 * Constructor with target State (toState) and shield being set.
 */
      public Transition(State toState, Shield shield) {
            super();
            this.shield = shield;
            this.toState = toState;
      }
      public State getToState() {
            return toState;
      }
      public void setToState(State toState) {
            this.toState = toState;
      }
      public Shield getShield() {
            return shield;
      }
      public void setShield(Shield shield) {
            this.shield = shield;
      }
}

State Machine

All of these pieces are stitched together in StateMachine. StateMachine is the backbone of state management in IRIS and knows the start state, the list of defined states, all the state transitions that are defined in the IRIS configuration, and has methods to add a state, add a transition, and most importantly, trigger the execution of the execute method upon successful transition:
/*
 * StateMachine is the backbone class for IRIS state management. It contains start state, a map of states,
 * and a map of state transitions, all of which are defined in the Iris Configuration.
 */
public class StateMachine {
      /*
       * In start state, there will always be a predefined start state which will be the initial conversation state.
       * Start state is initialized in Iris configuration class.
       */
      private State startState;
      // A map of all the defined states.
      private HashMap<String, State> states = new HashMap<String, State>();
      // A map of transition key and a list of possible transitions.
      private HashMap<String, List<Transition>> stateTransitions = new HashMap<String, List<Transition>>();
      public void setStartState(State state) {
            this.startState = state;
      }
      // Method to add states in the state map.
      private void addState(State state) {
            states.put(state.getName(), state);
            if (startState == null) {
                   startState = state;
            }
      }
      /*
       *  The addTransition method is used to add a transition from one state to another. It requires intent name, from state, and to state to define the transition.
       */
      public void addTransition(String intentName, State fromState, State toState) {
// When no Shield is passed, it is passed as null.
            addTransition(intentName, fromState, toState, null);
      }
      /*
       * Overloaded addTransition method that is similar to above but Shield is to be validated for this transition.
       */
      public void addTransition(String intentName, State fromState, State toState, Shield shield) {
            if (!states.containsKey(fromState.getName())) {
                   addState(fromState);
            }
            if (!states.containsKey(toState.getName())) {
                   addState(toState);
            }
            String key = makeTransitionKey(intentName, fromState);
            List<Transition> transitionList = stateTransitions.get(key);
            if (transitionList == null) {
                   transitionList = new ArrayList<Transition>();
                   stateTransitions.put(key, transitionList);
            }
            transitionList.add(new Transition(toState, shield));
      }
      /*
       * This method is the heart of the state machine. It receives the matched intent as an input along with session to know the current state. It then does a series of things: obtains the current state from a session or initializes the start state if no current state, then gets the matched intent, generates a transition key to look up in the transition map, and finally triggers the execute method of the target state and updates the state in session.
       */
      public String trigger(final MatchedIntent matchedIntent, final Session session) {
            State currentState = startState;
// Gets the current state from the session. If it is a new session, this will be null.
            String currentStateName = (String) session.getAttribute("currentStateName");
            if (currentStateName != null) {
                   currentState = states.get(currentStateName);
// At this point, the current state should not be null, and hence an exception is thrown as the handling of this condition is unknown.
                   if (currentState == null) {
                         throw new IllegalStateException("Illegal current state in session:" + currentStateName);
                   }
            }
            Intent intent = matchedIntent.getIntent();
            String intentName = (intent != null) ? intent.getName() : null;
// intent should not null here as it is expected that the matched intent will be an intent from the defined intent list.
            if (intentName == null) {
                   throw new IllegalArgumentException("Request missing intent." + matchedIntent.toString());
            }
// Generate transition key by using the pattern "intentname-statename".
            String key = makeTransitionKey(intentName, currentState);
// Get the target state transition list from the state transitions map.
            List<Transition> transitionToStateList = stateTransitions.get(key);
            /*
             * If there is a condition where the intent is valid and the current state is valid but there is no transaction defined, and if there is no definition of where to go, it is an illegal state condition and cannot be handled.
             */
            if (transitionToStateList == null) {
                   throw new IllegalStateException("Could not find state to transition to. Intent: " + intentName
                               + " Current State: " + currentState);
            }
            State transitionToState = null;
// Find first matching to-state and check shield conditions. This method iterates one by one to find a successful transition target state.
            for (Transition transition : transitionToStateList) {
                   if (transition.getShield() == null) {
// If there is no shield condition and there is a valid transition, assign the transitionToState as that target state.
                         transitionToState = transition.getToState();
                         break;
                   } else {
// If there is a shield condition, it will be validated and upon successful validation, the target state will be assigned as transitionToState.
                         if (transition.getShield().validate(matchedIntent, session)) {
                               transitionToState = transition.getToState();
                               break;
                         }
                   }
            }
// If state machine didn't find any matching states, it is an illegal state as it is not defined.
            if (transitionToState == null) {
                   throw new IllegalStateException("Could not find state to transition to. Failed all guards. Intent: "
                               + intentName + " Current State: " + currentState);
            }
// Action to be performed upon successful transition and response returned.
            String response = transitionToState.execute(matchedIntent, session);
// Current state is now updated in the session.
            session.setAttribute("currentStateName", transitionToState.getName());
            return response;
      }
      /*
       * Transition key is defined to store transition key and a list of transitions.
       */
      private String makeTransitionKey(String intentName, State state) {
            return intentName + '-' + state.getName();
      }
}

We are done with defining the base classes and their implementations. Until now whatever we discussed formed the core of the IRIS framework. Now let’s go further with a sample business use case and use the details to create specific intent classes, their slots, different states, and their possible transitions.

Building a Custom Chatbot for an Insurance Use Case

We discussed in Chapter 1 some of the most common applications of a chatbot in the life insurance industry. Now that we have some idea of the IRIS core, let’s dive into building an insurance-focused chatbot using the IRIS framework.

At the end of this exercise, our chatbot should be capable of providing
  • Account balance

  • Life insurance quotation

  • Claim status

  • An advisor

  • Answers to general enquiries

  • Market trends

  • Stock prices

  • Weather details

The high-level functional architecture is described in Figure 6-4. There are communication client channels such as Facebook Messenger, web chat, and Alexa, via which the users can connect to IRIS. In Figure 6-4, a channel integration module acts as a gateway module. It integrates with services such as Facebook Messenger, receives the request, and delegates the request for IRIS to respond. The response is sent back to Messenger by this module.

Then there is the IRIS Engine Core that handles all the domain-specific business logic that controls the behavior of the chatbot platform as well as defines and manages the transition from one state to another. Core connects with the intent classification engine that predicts the intent from a user’s utterance. The capabilities of IRIS that we discussed require an information retrieval module that can query its semantic knowledge base, a quotation service that provides life insurance quotations based on user inputs, a website search service, a user module that connects to a user database to get account balance information, a claims module to fetch claim details, and other third-party services to get market trends, stock prices, and weather information.
../images/478492_1_En_6_Chapter/478492_1_En_6_Fig4_HTML.jpg
Figure 6-4

High-level function architecture

We require the following intents to be defined for our example use case:
  • AccountBalanceIntent

  • AskForQuoteIntent

  • ClaimStatusIntent

  • ExitIntent

  • FindAdvisorIntent

  • GeneralQueryIntent

  • GetAccTypeIntent

  • GetClaimIdIntent

  • MarketTrendIntent

  • StockPriceIntent

  • WeatherIntent

Creating the Intents

Let’s create a User Intent class named AccountBalanceIntent:
public class AccountBalanceIntent extends Intent {
      public AccountBalanceIntent() {
            super("accountBalanceIntent");
      }}

All the other intent classes are created in the same way with their intent names. Some of these intents will have one or more slots defined as well.

AskForQuoteIntent requires four slots to provide a life insurance quote in our example:
  • Age (CustomNumericSlot type)

  • Height (CustomNumericSlot type)

  • Smoker (BooleanLiteralSlot type)

  • Weight (CustomNumericSlot type)

AccountBalanceIntent requires two slots: account type for which the account balance is required and a user PIN to authenticate the user. ipin is just a way of demonstrating how a very basic authentication can be performed. In an actual implementation, more complex forms of authentication should be used.
  • Account Type (AccTypeSlot type)

  • ipin (IPinSlot type)

ClaimStatusIntent and GetClaimIdIntent intents require claimId (AlphaNumericSlot type)

CustomNumericSlot

Next, let’s see the code showing how the above mentioned slot types are implemented for providing a simple approach to fulfill our requirements:
/*
 * Custom numeric slot
 */
public class CustomNumericSlot extends Slot {
      private String name;
      public CustomNumericSlot(String name) {
            super();
            this.name = name;
      }
      /*
       * match CustomNumericSlot takes user utterance and returns MatchedSlot if there is a slot match.
       * In this method we use regex and hard-coded words to number logic to identify if there is a number.
       * example - 18, eighteen
       */
      public MatchedSlot match(String utterance) {
            String token = utterance.replaceAll("[^0-9]+", "");
            if (token.isEmpty()) {
                   token = String.valueOf(wordStringToNumber(utterance));
            }
            return new MatchedSlot(this, token, token.toLowerCase());
      }
/*
* This method converts words to numbers. The logic is derived from https://stackoverflow.com/questions/26948858/converting-words-to-numbers-in-java.
*/
      public Number wordStringToNumber(String wordString) {
            if (wordString == null || wordString.length() < 1) {
                   return null;
            }
            wordString = wordString.replaceAll("-", " ");
            wordString = wordString.replaceAll(",", " ");
            wordString = wordString.toLowerCase().replaceAll(" and", " ");
            String[] splittedParts = wordString.trim().split("\s+");
            long finalResult = 0;
            long result = 0;
            for (String str : splittedParts) {
                   if (str.equalsIgnoreCase("zero")) {
                         result += 0;
                   } else if (str.equalsIgnoreCase("one")) {
                         result += 1;
                   } else if (str.equalsIgnoreCase("two")) {
                         result += 2;
                   } else if (str.equalsIgnoreCase("three")) {
                         result += 3;
                   } else if (str.equalsIgnoreCase("four")) {
                         result += 4;
                   } else if (str.equalsIgnoreCase("five")) {
                         result += 5;
                   } else if (str.equalsIgnoreCase("six")) {
                         result += 6;
                   } else if (str.equalsIgnoreCase("seven")) {
                         result += 7;
                   } else if (str.equalsIgnoreCase("eight")) {
                         result += 8;
                   } else if (str.equalsIgnoreCase("nine")) {
                         result += 9;
                   } else if (str.equalsIgnoreCase("ten")) {
                         result += 10;
                   } else if (str.equalsIgnoreCase("eleven")) {
                         result += 11;
                   } else if (str.equalsIgnoreCase("twelve")) {
                         result += 12;
                   } else if (str.equalsIgnoreCase("thirteen")) {
                         result += 13;
                   } else if (str.equalsIgnoreCase("fourteen")) {
                         result += 14;
                   } else if (str.equalsIgnoreCase("fifteen")) {
                         result += 15;
                   } else if (str.equalsIgnoreCase("sixteen")) {
                         result += 16;
                   } else if (str.equalsIgnoreCase("seventeen")) {
                         result += 17;
                   } else if (str.equalsIgnoreCase("eighteen")) {
                         result += 18;
                   } else if (str.equalsIgnoreCase("nineteen")) {
                         result += 19;
                   } else if (str.equalsIgnoreCase("twenty")) {
                         result += 20;
                   } else if (str.equalsIgnoreCase("thirty")) {
                         result += 30;
                   } else if (str.equalsIgnoreCase("forty")) {
                         result += 40;
                   } else if (str.equalsIgnoreCase("fifty")) {
                         result += 50;
                   } else if (str.equalsIgnoreCase("sixty")) {
                         result += 60;
                   } else if (str.equalsIgnoreCase("seventy")) {
                         result += 70;
                   } else if (str.equalsIgnoreCase("eighty")) {
                         result += 80;
                   } else if (str.equalsIgnoreCase("ninety")) {
                         result += 90;
                   } else if (str.equalsIgnoreCase("hundred")) {
                         result *= 100;
                   } else if (str.equalsIgnoreCase("thousand")) {
                         result *= 1000;
                         finalResult += result;
                         result = 0;
                   } else if (str.equalsIgnoreCase("million")) {
                         result *= 1000000;
                         finalResult += result;
                         result = 0;
                   } else if (str.equalsIgnoreCase("billion")) {
                         result *= 1000000000;
                         finalResult += result;
                         result = 0;
                   } else if (str.equalsIgnoreCase("trillion")) {
                         result *= 1000000000000L;
                         finalResult += result;
                         result = 0;
                   } else {
                         // unknown word
                         return null;
                   }
            }
            finalResult += result;
            result = 0;
            return finalResult;
      }
      @Override
      public String getName() {
            return name;
      }
}

BooleanLiteralSlot

Here we’ve highlighted a match method snippet of the BooleanLiteralSlot class :
/*
* match method of BooleanLiteralSlot. We need to recognize if the user meant no or yes in any which way. One of the simplest ways to implement this is to verify by string matching the most commonly used words.
*/
      @Override
      public MatchedSlot match(String utterance) {
            if (utterance.toLowerCase().contains("yes") || utterance.toLowerCase().contains("yeah")
                         || utterance.toLowerCase().contains("ya") || utterance.toLowerCase().contains("yup")) {
                   return new MatchedSlot(this, utterance, "yes");
            } else if (utterance.toLowerCase().contains("no") || utterance.toLowerCase().contains("na")
                         || utterance.toLowerCase().contains("nopes") || utterance.toLowerCase().contains("noo")
                         || utterance.toLowerCase().contains("nope") || utterance.toLowerCase().contains("dont")
                         || utterance.toLowerCase().contains("don't") || utterance.toLowerCase().contains("do not")) {
                   return new MatchedSlot(this, utterance, "no");
            }
            return null;
      }

AccTypeSlot

AccTypeSlot is implemented to understand the account type. If no slot match happens, the state engine will reprompt as the system could not identify the account type:
      /*
       * For intent where we want to understand what type of account balance the user is looking for, a straightforward method is to apply a
       * string match of possible account types. Since we are only looking for whether the utterance contains any of those keywords,
       * all of the below possibilities are covered:
       * i am looking for annuities account balance
       * annuities
       * annuities balance
       * tell 401k balance
       * want my retirement balance etc.
       */
      @Override
      public MatchedSlot match(String utterance) {
            if (utterance.toLowerCase().contains("annuities") || utterance.toLowerCase().contains("annuity")) {
                   return new MatchedSlot(this, "annuities", "annuities");
            } else if (utterance.toLowerCase().contains("401k") || utterance.toLowerCase().contains("retirement")
                         || utterance.toLowerCase().contains("401") || utterance.toLowerCase().contains("401 k")) {
                   return new MatchedSlot(this, "401k", "401k");
            }
            return null;
      }

IPinSlot

In an actual implementation, this type of slot may not be defined but to highlight how a basic authentication can be implemented, we use this entity. We have considered in the example that users will have their own ipin generated in some way and stored in the back end and that it will be a six- digit number. In a real world, a much more complex number and a set of authentication mechanisms will exist such as username, password, and ZIP code.

In the method snippet below, if the value is 123456, only then will the account balance be displayed. Any other number will result in a wrong ipin provided by the user.

Warning

Never implement such a weak authentication system. It will compromise your enterprise security. The purpose here is to only complete the flow of discussion. In no way do we endorse such weak authentication.

@Override
      public MatchedSlot match(String token) {
            if (token.matches("[0-9]+") && token.length() == 6 && token.equalsIgnoreCase("123456")) {
                   return new MatchedSlot(this, token, token);
            }
            return null;
      }

AlphaNumericSlot

As the name suggests, the entity is supposed to be alphanumeric, and if there is a word that is alphanumeric, MatchedSlot is returned. In the example, AphaNumericSlot is used for processing claims that are alphanumeric.
@Override
      public MatchedSlot match(String utterance) {
            /*
             * User utterance is split into utterance tokens. We need to see if there is any alphanumeric word in the utterance
             * This implementation is useful for scenarios mentioned below:
             * my claim id is gi123 can you tell the claim status
             * claim the status of abc123
             */
            ArrayList<String> utteranceTokens = new ArrayList<String>(Arrays.asList(utterance.split("\s+")));
            String claimId = null;
            for (String token : utteranceTokens) {
                   if (!token.matches("[a-zA-Z]+")) {
                         token = token.replace(".", "");
                         token = token.trim();
                         claimId = token;
                         return new MatchedSlot(this, claimId, claimId);
                   }
            }
            return null;
      }

Now that we have defined all the possible intents that will be classified by our intent classification service, defined slots, and slot type, let’s see what the IRIS configuration looks like. As explained, intents, intent matcher, slot matcher, and different slot and slot types are defined in the IrisConfiguration class.

IrisConfiguration

We put IrisConfiguration class in a separate package named com.iris.bot.config:
public class IrisConfiguration {
      public IntentMatcherService getIntentMatcherService() {
            CustomSlotMatcher slotMatcher = new CustomSlotMatcher();
            IntentMatcherService intentMatcherService = new IntentMatcherService(slotMatcher);
            Intent findAdvisorIntent = new FindAdvisorIntent();
            Intent askForQuoteIntent = new AskForQuoteIntent();
            // Slots for askForQuote intent fulfillment.
            askForQuoteIntent.addSlot(new CustomNumericSlot("age"));
            askForQuoteIntent.addSlot(new CustomNumericSlot("height"));
            askForQuoteIntent.addSlot(new CustomNumericSlot("weight"));
            askForQuoteIntent.addSlot(new BooleanLiteralSlot("smoked"));
            Intent generalQueryIntent = new GeneralQueryIntent();
            Intent stockPriceIntent = new StockPriceIntent();
            Intent marketTrendIntent = new MarketTrendIntent();
            Intent accountBalanceIntent = new AccountBalanceIntent();
            accountBalanceIntent.addSlot(new AccTypeSlot("accType"));
            accountBalanceIntent.addSlot(new IpinSlot("ipin"));
            Intent getAccTypeIntent = new GetAccTypeIntent();
            getAccTypeIntent.addSlot(new AccTypeSlot("accType"));
            getAccTypeIntent.addSlot(new IpinSlot("ipin"));
            Intent weatherIntent = new WeatherIntent();
            Intent claimStatusIntent = new ClaimStatusIntent();
            claimStatusIntent.addSlot(new AlphaNumericSlot("claimId"));
            Intent getClaimIdIntent = new GetClaimIdIntent();
            getClaimIdIntent.addSlot(new AlphaNumericSlot("claimId"));
            Intent exitIntent = new ExitIntent();
/*
 * All the intents we defined above are added to the intent matcher service.
 */
            intentMatcherService.addIntent(findAdvisorIntent);
            intentMatcherService.addIntent(askForQuoteIntent);
            intentMatcherService.addIntent(generalQueryIntent);
            intentMatcherService.addIntent(stockPriceIntent);
            intentMatcherService.addIntent(marketTrendIntent);
            intentMatcherService.addIntent(exitIntent);
            intentMatcherService.addIntent(getAccTypeIntent);
            intentMatcherService.addIntent(accountBalanceIntent);
            intentMatcherService.addIntent(weatherIntent);
            intentMatcherService.addIntent(claimStatusIntent);
            return intentMatcherService;
      }
      public StateMachine getStateMachine() {
      // discussed in detail below
       return null;
 }
}

Adding States

Before we add state machine configurations, let’s see how many possible states we have:
  1. 1.

    Start state

     
  2. 2.

    Ask for quote state

     
  3. 3.

    Get quote state

     
  4. 4.

    Find an advisor state

     
  5. 5.

    General query state

     
  6. 6.

    Stock price state

     
  7. 7.

    Market trend state

     
  8. 8.

    Get account balance state

     
  9. 9.

    Get account type state

     
  10. 10.

    Get weather state

     
  11. 11.

    Get claim status state

     
  12. 12.

    Exit state

     
The getStateMachine method is now added to IrisConfiguration with the states listed above:
public StateMachine getStateMachine() {
/*
Creates an instance of StateMachine that holds start state, a map of states, and a map of state transitions, all of which are defined below in IrisConfiguration.
 */
            StateMachine stateMachine = new StateMachine();
            State startState = new StartState();
            State askforQuoteState = new AskForQuoteState();
            State getQuoteState = new GetQuoteState();
            Shield haveQuoteDetailShield = new HaveQuoteDetailShield();
            Shield dontHaveQuoteDetailsShield = new DontHaveQuoteDetailsShield();
            State findAdvisorState = new FindAdvisorState();
            State generalQueryState = new GeneralQueryState();
            State stockPriceState = new StockPriceState();
            State marketTrendState = new MarketTrendState();
            State getAccountBalanceState = new GetAccountBalanceState();
            Shield haveAccTypeShield = new HaveAccTypeShield();
            Shield dontHaveAccTypeShield = new DontHaveAccTypeShield();
            State getAccTypeState = new GetAccTypeState();
            State getWeatherState = new GetWeatherState();
            State getClaimStatusState = new GetClaimStatus();
            Shield haveClaimIdShield = new HaveClaimIdShield();
            State getClaimIdState = new GetClaimIdState();
            State exitState = new ExitState();
/*
* Here we initialize the start state. The Start state execute method is never supposed to be called.
 */
            stateMachine.setStartState(startState);
/*
* We need to define state transitions here.
 */
 }

In getStateMachine method , we define the state classes and Shields.

Shields

As discussed, Shields provide a Boolean condition for transition from one state to another. If all the information required for transitioning to another state is available, Shields returns true.

We implement five shields in our example in the getStateMachine method , each implementing the validate method .

DontHaveAccTypeShield
We need accType and a valid ipin in order to transition to GetAccountBalanceState. This shield returns true if either of them is not provided.
public boolean validate(MatchedIntent match, Session session) {
            // save slots to session
            SessionStorage.saveSlotsToSession(match, session);
// Get all validation entities from session.
String accType = SessionStorage.getStringFromSlotOrSession(match, session, "accType", null);
            String ipin = SessionStorage.getStringFromSlotOrSession(match, session, "ipin", null);
// Returns true if accType or ipin is null.
            return (accType == null || ipin == null);
      }
DontHaveQuoteDetailsShield
We need age, smoker info, height, and weight to provide insurance quotation eligibility. This shield returns true if we do not have information on any of them. The state remains in AskForQuoteState until we have answers for all questions and then transitions to GetQuoteState.
Public boolean  validate(MatchedIntent match, Session session) {
// Save slots into session.
            SessionStorage.saveSlotsToSession(match, session);
            String age = SessionStorage.getStringFromSlotOrSession(match, session, "age", null);
            String smoked = SessionStorage.getStringFromSlotOrSession(match, session, "smoked", null);
            String height = SessionStorage.getStringFromSlotOrSession(match, session, "height", null);
            String weight = SessionStorage.getStringFromSlotOrSession(match, session, "weight", null);
/*
 * If we don't have all the slots fulfilled, we need to return true so that askForQuote state is executed again. As there are multiple questions asked in the askForQuote state, unless all questions are answered and all values populated, the state remains the same, unless the intent of the user changes.
 */
            return (age == null || smoked == null || height == null || weight == null);
      }
HaveAccTypeShield
This shield returns true if both accType and ipin are available from the user and allows a transition to the GetAccountBalanceState:
Public boolean validate(MatchedIntent match, Session session) {
            SessionStorage.saveSlotsToSession(match, session);
            String accType = SessionStorage.getStringFromSlotOrSession(match, session, "accType", null);
            String ipin = SessionStorage.getStringFromSlotOrSession(match, session, "ipin", null);
// Returns true only if both accType and ipin are available.
            return (accType != null && ipin != null);
      }
HaveClaimIdShield
This shield returns true if claimId is not null and hence allows a transition to the GetClaimStatus state :
public boolean validate(MatchedIntent request, Session session) {
            SessionStorage.saveSlotsToSession(request, session);
            String claimId = SessionStorage.getStringFromSlotOrSession(request, session, "claimId", null);
// Returns true only if claimId is not null.
            return (claimId != null);
      }
HaveQuoteDetailShield
This shield returns true if all the values required to transition to GetQuoteState are present:
public boolean validate(MatchedIntent match, Session session) {
            // Saves slots to session.
            SessionStorage.saveSlotsToSession(match, session);
            // Gets all validation entities from session.
            String age = SessionStorage.getStringFromSlotOrSession(match, session, "age", null);
            String smoked = SessionStorage.getStringFromSlotOrSession(match, session, "smoked", null);
            String height = SessionStorage.getStringFromSlotOrSession(match, session, "height", null);
            String weight = SessionStorage.getStringFromSlotOrSession(match, session, "weight", null);
            //Returns true if all values exist, else return false.
            return (age != null && smoked != null && height != null && weight != null);
      }
We have two more things yet to be discussed to complete IrisConfiguration related concepts:
  • The execute method of each state

  • State transitions

Adding Execute Methods

Let’s start by implementing the execute method of each state described in the example.

Exit State
The execute method of ExitState responds with a simple reply. In an actual implementation, it could also support saving the session and context to a persistent storage before resetting them.
public class ExitState extends State {
      public ExitState() {
            super("exitState");
      }
/*
 * When this state is reached, the execute method is invoked. As a result, a reply is sent back.
 */
      @Override
      public String execute(MatchedIntent matchedIntent, Session session) {
            String reply = "Anything else that I may help you with?";
            return reply;
      }
}
FindAdvisorState
The execute method of FindAdvisorState would typically call a search API with required parameters such as advisor name, ZIP code, etc. to return relevant advisors to the user. We demonstrate how to reach here but we skip the implementation.
public String execute(MatchedIntent matchedIntent, Session session) {
            String reply = "You know what, I dont have the data about financial advisors with me."
                         + " But I hope you do get the point that I could have surely provided it to you if I was connected to a database. "
                         + "I will let my boss know that you were asking for it. Next time you wont be disappointed, I promise. Here, ask me anything else for now please!";
            return reply;
      }
GetAccountBalanceState
The execute method of this state returns the account balance. Transition to this state only happens when the shield validates that we have the account type and ipin.
      Public String execute(MatchedIntent matchedIntent, Session session) {
            String reply = null;
            Random rand = new Random();
            String accType = SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "accType", null);
            if (accType.equalsIgnoreCase("Annuities")) {
/*
* In a real-world implementation, we would call a service or query a database to get the account balance. For the sake of implementation here, we are returning a random integer .
*/
                   reply = "Your Annuities account balance is: " + (rand.nextInt(1000) + 100) + "."
                               + " Anything else that I can do for you? ";
            } else if (accType.equalsIgnoreCase("401k"))
                   reply = "Your 401K account balance is: " + (rand.nextInt(4000) + 500) + "."
                               + " Anything else that you want to know? ";
            else
                   reply = "Sorry, I am not able to retrieve your " + accType + " balance right now. How else can I help you? ";
/*
* Slot details saved in session attributes previously are now removed. We cannot store these details even at a session level as the user may request for account balance again, but this time he may need balance details of a different type of account. However, we still store these values in session until we reach here so that we know that this information have been answered by user and shields can then validate.
*/
            session.removeAttribute("acctype");
            session.removeAttribute("getaccTypeprompt");
            session.removeAttribute("getipinprompt");
            session.removeAttribute("ipin"s);
            return reply;
      }
GetAccTypeState
The execute method of this state prompts for the ipin and account type information to be provided by user.
      public String execute(MatchedIntent matchedIntent, Session session) {
            SessionStorage.saveSlotsToSession(matchedIntent, session);
            String reply = null;
            if (SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "ipin", null) == null) {
                   if (SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "getipinprompt", null) == null)
                         reply = "Sure I will help you with that! Since this is a confidential information, I will need additional details to verify "
                                   + "your identity. Can you tell me your 6 digits IPIN please?";
                   else
                       reply = "Either you have not entered 6 digits code or the IPIN entered by you is incorrect. Please verify and type again !";
                   session.setAttribute("getipinprompt", "flag1");
            }
            else if (SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "accType", null) == null) {
                   if (SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "getaccTypeprompt", null) == null)
                         reply = "Your IPIN was successfully verified. Are you looking for Annuities balance or 401k account balance?";
                   else
                         reply = "I did not understand that. Did you say annuities or 401k?";
                   session.setAttribute("getaccTypeprompt", "flag1");
            }
            return reply;
      }
GetClaimIdState
The execute method of this state obtains a claim ID from the user’s utterance and sets it in the session attribute.
      public String execute(MatchedIntent matchedIntent, Session session) {
            SessionStorage.saveSlotsToSession(matchedIntent, session);
            if (SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "claimId", null) == null) {
                   if (SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "getclaimidprompt", null) == null)
                         reply = "No Problem. Could you tell me the Claim Id Please?";
                   else
                         reply = "Sorry, I did not get the claim ID. Can you please re-enter it?";
            }
            session.setAttribute("getclaimidprompt", "flag1");
            return reply;
      }
AskForQuote State
The execute method of this state gets age, smoker info, height, and weight from user. It also stores the last question asked to map it back to the follow up answer.
      public String execute(MatchedIntent matchedIntent, Session session) {
            SessionStorage.saveSlotsToSession(matchedIntent, session);
// Default reply
            String reply = "I am having trouble understanding...";
// Checking for age
            if (SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "age", null) == null) {
// Age is set in session to be the last question asked in askQuote at this point.
                   session.setAttribute("askQuoteLastQuestion", "age");
/*
* To differentiate between whether we are asking this question for the first time or we asked before and the user didn't answer, we use "getageprompt." If the "getageprompt" value is null, we have not asked this question to the user before in that particular session. It helps to differentiate the reply message.
 */
if (SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "getageprompt", null) == null)
                         reply = "Sure, I will help you with that. May I know your age?";
                   else
/*
* Let's say we are expecting that the user will enter his age and that is the current question in conversation. However, instead of replying age, the user changes the intent by asking about the weather. IRIS is designed to handle intent switches from one context to another. But, next time, if the user desires to get a quote again, we will not ask questions already answered and even the ask message will be different, just like how its mentioned in the if-else reply message here.
 */
                         reply = "I am not sure if I got your age right last time. Please type again";
// Setting getageprompt in session to note that age has been asked before.
                   session.setAttribute("getageprompt", "flag1");
// Same logic applies for whether the user answered to his smoking status or not.
            } else if (SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "smoked", null) == null) {
                   session.setAttribute("askQuoteLastQuestion", "smoked");
                   if (SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "getsmokedprompt", null) == null)
                         reply = "Have you smoked in the last 12 months?";
                   else
                         reply = "Last time you did not tell me if you smoked in the last 12 months, Have you?";
                   session.setAttribute("getsmokedprompt", "flag1");
// Same logic applies for height.
            } else if (SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "height", null) == null) {
                   session.setAttribute("askQuoteLastQuestion", "height");
                   if (SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "getheightprompt", null) == null)
                         reply = "What's your height (in centimeters)?";
                   else
                         reply = "What's your height (in centimeters)? Please help me understand again?";
                   session.setAttribute("getheightprompt", "flag1");
// Lastly, same logic for weight.
            } else if (SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "weight", null) == null) {
                   session.setAttribute("askQuoteLastQuestion", "weight");
                   if (SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "getweightprompt", null) == null)
                         reply = "What's your weight (in pounds)?";
                   else
                         reply = "Tell me your weight in pounds again. I did not get it the last time";
                   session.setAttribute("getweightprompt", "flag1");
            }
            return reply;
      }
GetQuote State
The execute method of this state provides quotation eligibility based on the age, smoker info, height, and weight. The method implements a simple business logic to calculate if the user is eligible or not. However, in a real-world scenario, more complex business logic exists and all of this information will be passed to another API that will provide the eligibility information.
Public String execute(MatchedIntent matchedIntent, Session session) {
            SessionStorage.saveSlotsToSession(matchedIntent, session);
            Boolean eligible = true;
            String answer = "";
            int age = Integer.parseInt(SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "age", null));
            String smoked = SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "smoked", null);
            int weight = Integer.parseInt(SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "weight", null));
            int height = Integer.parseInt(SessionStorage.getStringFromSlotOrSession(matchedIntent, session, "height", null));
            /*
             * Checking business logic and calculating BMI (body mass index).
             * In the example, eligibility is defined based on whether BMI is less than or greater than 33.
             */
            if (age > 60 || age < 18)
                   eligible = false;
            if (smoked.equalsIgnoreCase("yes"))
                   eligible = false;
            double weightInKilos = weight * 0.453592;
            double heightInMeters = ((double) height) / 100;
            double bmi = weightInKilos / Math.pow(heightInMeters, 2.0);
            if (bmi > 33)
                   eligible = false;
            if (eligible) {
                   answer = "Great News! You are eligible for an accelerated UW Decision. Please proceed with your application "
                               + "at this link: https://www.dummylink.com "
                               + "Anything else that I could help you with?";
            } else {
                   answer = "Unfortunately, You are not eligible for an Accelerated UW Decision. Please register at https://www.dummylink.com "
                               + "and our representatives will contact with you shortly to further process your application "
                               + "Anything else that I could help you with?";
            }
/*
* Remove attributes stored in the session. All of these four attributes are treated as short term in the example. We used the session also to store details of which slots to prompt for and which not to based on whether user answered them .
*/
            session.removeAttribute("getageprompt");
            session.removeAttribute("getsmokedprompt");
            session.removeAttribute("getheightprompt");
            session.removeAttribute("getweightprompt");
            session.removeAttribute("askquotelastquestion");
            session.removeAttribute("height");
            session.removeAttribute("age");
            session.removeAttribute("smoked");
            session.removeAttribute("weight");
            return answer;
      }
Start State
This state is the starting state and the “current” state by default when the user interacts in a new session. A start state is never executed due to the result of any behavior.
public String execute(MatchedIntent matchedIntent, Session session) {
            throw new IllegalStateException("You shouldn't be executing this state!");
      }
GeneralQuery State

We mentioned that in a chatbot where there are multiple intents such as a user looking for account balance, claim status, weather details, life insurance quote, etc., the general query is not an explicit intent. We classify an utterance into a general query if no other intent matches explicitly.

In the general query state, we perform two steps:
  1. 1.

    Match if a user utterance is a question that has an answer in our knowledge repository. The knowledge repository is where the most frequently asked questions and their answers are stored. A knowledge repository could also have general user information parsed and stored in a way that can be queried to find a meaningful answer. A knowledge repository could be represented in the form of a graph, RDF semantic web, or implemented using a simple search engine.

     
  2. 2.

    If there is no matching answer in our knowledge repository, we perform a search on our portal to find any matching result that could be replied to the user. If there is no response from the search service as well, we reply to the user saying we cannot help on this ask because we don’t have much information about it now.

     
public String execute(MatchedIntent matchedIntent, Session session) {
            String answer = "I am so Sorry, I do not have any information related to your query. Can I help you with something else?";
            String uri = "https://www.dummy-knowledge-base-service-url?inputString=";
            uri = uri + matchedIntent.getUtterance();
            RestTemplate restTemplate = new RestTemplate();
            String result = restTemplate.getForObject(uri, String.class);
            ObjectMapper mapper = new ObjectMapper();
            try {
                  if(result!=null){
/*
* If the result is not null and contains a response (answer), we parse that information and assign it to the answer variable answer = "PARSED-INFORMATION from result" + " Anything else that you would like to ask?";
 */
}
                   else{
/*
 * If no answer was obtained from the knowledge repository, then to back fill with some valid response, we call the enterprise search API and pass the utterance as a search string.
*/
                         uri = "https://www.my-enterprise-website.com/searchservice/fullsearch?&inputSearchString=";
/*
*  The utterance is added to the HTTP GET request. Depending on the implementation it could be GET or POST and the service may have different parameters.
*/
                         uri = uri + matchedIntent.getUtterance();
                         result = restTemplate.getForObject(uri, String.class);
                         mapper = new ObjectMapper();
                         try {
/*
* Here we try to parse the JSON response and if there is a result with a decent score returned from the search engine, we read the title and description of the result and add it before sending the response back.
*/
                               answer = "Sorry I do not have an exact answer to this right now. "
                                            + "You may get some details on the page - " + "TITLE OF THE PAGE OBTAINED FROM RESPONSE"
                                            + ". Click here -> " + "URL LINK" + " for more info."
                                            + " Anything else that you would like to ask?";
                         } catch (Exception e) {
                               answer = "I am so Sorry, I do not have any information related to your query. Can I help you with something else?";
                         }
                   }
            } catch (Exception e) {
                   e.printStackTrace();
                   answer = "I am so Sorry, I do not have any information related to your query. Can I help you with something else?";
            }
            return answer;
      }

Market trends, stock prices, weather state, and claim status state require integration with third-party data sources or connecting to a database. We will discuss this in the next chapter in detail.

Adding State Transitions

In the getStateMachine method of the IrisConfiguration class, we define the transitions from one state to another. For example, we can transition to any state from a start state, as explained in the following snippet. The first argument of the addTransition method is the intent name, the second argument is the current state, the third argument is the target state, and the fourth argument is an optional shield.

In this example, since we are defining transitions from startState and fromState, all transitions will be startState.
            /*
             * This transition says that if we are in the start state, and a generalQueryIntent is obtained,
             * we remain in the generalQueryState (and trigger execute method of this state).
             */
            stateMachine.addTransition("generalQueryIntent", startState, generalQueryState);
            /*
             * This transition says that if we are in the start state, and an askForQuoteIntent intent is obtained,
             * we change to the target state which is getQuoteState if the shield conditions are validated.
             * Else we check the next transition condition.
             */
            stateMachine.addTransition("askForQuoteIntent", startState, getQuoteState, haveQuoteDetailShield);
            /*
             * If the shield conditions are not validated for the askForQuoteIntent, it means that we do not have all the information
             * for switching to getQuoteState, which provides quote details. Hence, in that case, we switch to askforQuoteState without the need
             * of a shield.
             */
            stateMachine.addTransition("askForQuoteIntent", startState, askforQuoteState);
            stateMachine.addTransition("findAdvisorIntent", startState, findAdvisorState);
            stateMachine.addTransition("stockPriceIntent", startState, stockPriceState);
            stateMachine.addTransition("marketTrendIntent", startState, marketTrendState);
            /*
             * If we are in the start state and the user intends to get an account balance, we validate with a shield if we have an account type
             * and ipin details to switch to getAccountBalanceState and trigger its execute method.
             */
            stateMachine.addTransition("accountBalanceIntent", startState, getAccountBalanceState, haveAccTypeShield);
            /*
             * Otherwise, if shield does not validate, it means we do not have all the details and hence we switch to getAccTypeState
             * to get all the details.
             */
            stateMachine.addTransition("accountBalanceIntent", startState, getAccTypeState);
            stateMachine.addTransition("weatherIntent", startState, getWeatherState);
            stateMachine.addTransition("claimStatusIntent", startState, getClaimStatusState, haveClaimIdShield);
            stateMachine.addTransition("claimStatusIntent", startState, getClaimIdState);
Similarly, we can define state transitions from the findAdvisor state :
            stateMachine.addTransition("exitIntent", findAdvisorState, exitState);
            stateMachine.addTransition("marketTrendIntent", findAdvisorState, marketTrendState);
            stateMachine.addTransition("findAdvisorIntent", findAdvisorState, findAdvisorState);
            stateMachine.addTransition("askForQuoteIntent", findAdvisorState, askforQuoteState);
            stateMachine.addTransition("generalQueryIntent", findAdvisorState, generalQueryState);
            stateMachine.addTransition("weatherIntent", findAdvisorState, getWeatherState);
            stateMachine.addTransition("claimStatusIntent", findAdvisorState, getClaimStatusState, haveClaimIdShield);
            stateMachine.addTransition("claimStatusIntent", findAdvisorState, getClaimIdState);
            stateMachine.addTransition("accountBalanceIntent", findAdvisorState, getAccountBalanceState, haveAccTypeShield);
            stateMachine.addTransition("accountBalanceIntent", findAdvisorState, getAccTypeState);
            stateMachine.addTransition("stockPriceIntent", findAdvisorState, stockPriceState);
Let’s see what the state transitions from GetAccountBalance state are:
            stateMachine.addTransition("accountBalanceIntent", getAccountBalanceState, getAccountBalanceState, haveAccTypeShield);
            stateMachine.addTransition("accountBalanceIntent", getAccountBalanceState, getAccTypeState);
            stateMachine.addTransition("askForQuoteIntent", getAccountBalanceState, askforQuoteState);
            stateMachine.addTransition("marketTrendIntent", getAccountBalanceState, marketTrendState);
            stateMachine.addTransition("findAdvisorIntent", getAccountBalanceState, findAdvisorState);
            stateMachine.addTransition("stockPriceIntent", getAccountBalanceState, stockPriceState);
            stateMachine.addTransition("weatherIntent", getAccountBalanceState, getWeatherState);
            stateMachine.addTransition("claimStatusIntent", getAccountBalanceState, getClaimStatusState, haveClaimIdShield);
            stateMachine.addTransition("claimStatusIntent", getAccountBalanceState, getClaimIdState);

Similarly, we can create transitions for other states.

However, note that there is a difference in state transitions defined by GetAccountBalanceState and FindAdvisorState. You can go to GeneralQueryState from FindAdvisorState but not from GetAccountBalanceState. This is where we define which transitions are possible from each state. In the example here, we don’t want users to be asking general questions after they ask for account balance details.

A better explanation of this is when the user wants to know his account balance. IRIS will prompt the user on whether he/she wants to know the retirement account balance or annuities account balance and should be expecting a response like one of the following:
  • I am looking for a retirement account balance

  • Retirement

  • 401k balance

  • Annuities balance

  • Want to know 401k account balance

  • 401k

  • annuities

Now, in responses to “annuities” or “401k,” it is difficult to understand whether the intent is to respond to the question asked or if the user switched the context and is asking something very general that can be searched by IRIS in its knowledge base. For example, a user can also respond with something like
  • What’s the weather in Dublin

  • My claim ID is abc123 can you tell the claim status

  • Insurance

  • 401k

  • Retirement funds

Now, contextually, it is difficult to differentiate between a general query search vs. a response to an account type. Here, we can decide that we will not allow a transition to GeneralQueryState.

Another question is how to understand if it is a general query ask. A general query is never an intent. If no other intent, such as asking for weather details, stock price, market trend, claim status, etc., is applicable and the intent classification engine is not able to classify the user utterance into any of these intent categories with high probability, we by default switch it to a general query.

At this stage, we are done with implementing the IrisConfiguration class, and we have defined intents, matchers, slot, slot types, states, different state transitions, and shields in this class.

Managing State

We now need a helper layer that holds this configuration and seamlessly performs intent matching and then passes this information to trigger state machine actions. This helper layer is StateMachineManager:
public class StateMachineManager {
      /** The intent matcher service for IRIS bot. */
      protected IntentMatcherService intentMatcherService;
      /** The state machine */
      protected StateMachine stateMachine;
      /**
       * Constructs the bot by passing a configuration class that sets up the intent matcher service and state machine.
       *
       */
      public StateMachineManager(IrisConfiguration configuration) {
            intentMatcherService = configuration.getIntentMatcherService();
            stateMachine = configuration.getStateMachine();
      }
      public String respond(Session session, String utterance) throws Exception {
            try {
                   /*
                    * Invokes the intentMatcherService.match method that returns matched intent.
                    * This method sends the user utterance and session as an input and obtains matched intent from the intent classification service.
                    */
                   MatchedIntent matchedIntent = intentMatcherService.match(utterance, session);
                   /*
                    * This method sends the matched intent as an input along with session and gets the response back from the state machine.
                    */
                   String response = stateMachine.trigger(matchedIntent, session);
                   // The response is returned.
                   return response;
            } catch (IllegalStateException e) {
                   throw new Exception("Hit illegal state", e);
            }
      }

At this point, we have IRIS ready for the insurance industry. However, to make it functional, we expose it as a REST service. We need to create a ConversationRequest, a ConversationResponse, a ConversationService, and a ConversationController.

Exposing a REST Service

IRIS is exposed as a REST service and accepts HTTP GET requests. The following is an example of the service running on localhost on port 8080 to accept HTTP GET requests:
http://localhost:8080/respond?sender=sender-id&message=user-message
This is a JSON representation:
{"message":"response-message-from-service"}

ConversationRequest

We create a sample ConversationRequest class in the com.iris.bot.request package that a front-end client can use to send a request to the IRIS bot engine back end via an integration module. In the example client, we integrate with Facebook Messenger. Facebook Messenger provides a sender id, which is a unique user id. This single user identifier helps in creating and maintaining sessions for the user and storing long-term and short-term attributes in memory.
public class ConversationRequest {
      /**
       * Sender id as per Facebook.
       */
      private String sender;
      /**
       * Actual text message.
       */
      private String message;
      /**
       * Timestamp sent by Facebook.
       */
      private Long timestamp;
      /**
       * Sequence number of the message.
       */
      private Long seq;
}

ConversationResponse

ConversationResponse is created in the com.iris.bot.response package. The response from the IRIS bot is sent to the integration module through an object of this class.
public class ConversationResponse {
      /**
       * Actual reply from the bot
       */
      private String message;
}

ConversationService

ConversationService creates a static instance of the state machine manager, which is passed to IrisConfiguration in constructor arguments. It further creates a static instance of the SessionStorage class. These classes are created as static because only one instance of this class should be instantiated. Further, the singleton design pattern could also be used to design these single instances. ConversationService calls a respond method of StateMachineManager and returns a response to the controller. The controller calls a getResponse method of ConversationService by passing the ConversationRequest.
public class ConversationService {
      private static StateMachineManager irisStateMachineManager = new StateMachineManager(new IrisConfiguration());
      private static SessionStorage userSessionStorage = new SessionStorage();
      public ConversationResponse getResponse(ConversationRequest req) {
// Default response to be sent if there is a server side exception.
            String response = "Umm...I apologise. Either I am not yet trained to answer that or I think I have had a lot of Guinness today. "
                         + "I am unable to answer that at the moment. " + "Could you try asking something else Please !";
// If the request message is a salutation like hi or hello, then instead of passing this information to statemachine manager, a hard-coded response to salutation can be returned from the service layer.
            if (req.getMessage().equalsIgnoreCase("hi") || req.getMessage().equalsIgnoreCase("hey iris")) {
                   response = "Hi There! My name is IRIS (isn't it nice! My creators gave me this name). I am here to help you answer your queries, get you the status of your claims,"
                               + " tell you about stock prices, find you a financial advisor, inform you about current market trends, help you check your life insurance eligibility "
                               + "or provide you your account balance information. "
                               + "Hey, you know what, I can also tell you about current weather in your city. Try asking me out ! ";
            }
// Gets the session object for the sender of the request.
            Session session = userSessionStorage.getOrCreateSession(req.getSender());
// Creates a response object.
            ConversationResponse conversationResponse = new ConversationResponse();
            try {
// Calls the respond method of state manager by passing session and message (user utterance).
                   response = irisStateMachineManager.respond(session, req.getMessage());
// Response is set to the the conversationResponse and returned to the controller.
                   conversationResponse.setMessage(response);
            } catch (Exception e) {
                   conversationResponse.setMessage(response);
            }
            return conversationResponse;
      }
}

ConversationController

Finally, there’s the controller that exposes ConversationService as a REST API by creating an endpoint /respond. The implementation is straightforward: the controller receives a GET request, it passes to the service, and the service responds with the response message.

Adding a Service Endpoint

Let’s create REST service endpoint using Spring Boot. In Spring’s approach to building RESTful web services, HTTP requests are handled by a controller. These components are easily identified by the @RestController annotation . The @RequestMapping annotation ensures that HTTP requests to /respond are mapped to the getKeywordresults() method.

More on how to build a RESTful web service using Java and Spring can be found at https://spring.io/guides/gs/rest-service/ .
@RestController
public class ConversationController {
      @Autowired
      ConversationService conversationService;
      @RequestMapping(value = "/respond", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
      @ResponseBody
      public  ConversationResponse getKeywordresults(@ModelAttribute ConversationRequest request) {
            return conversationService.getResponse(request);
      }
}
If we run this on localhost, a sample GET request will be
http://localhost:8080/respond?sender=SENDER_ID&message=USER_MESSAGE&timestamp=TIMESTAMP&seq=MESSAGE_SEQUENCE

We create attributes of ConversationRequest based on attributes that are sent by Facebook. Hence in the request structure we have timestamp and seq. However, we do not make use of these two attributes in the demo implementation for intent classification or state transition. Note that these attributes of Messenger webhook events may change with new versions of the Facebook API and can be used in your code depending on your requirements.

Summary

Let’s summarize what we discussed in this chapter. We started with the idea of building a basic chatbot framework and why a custom designed chatbot is a needed for the enterprise. Then we discussed the core components of the framework.

First, we discussed intents, utterances, and slots, and defined a custom intent and slot matcher. We also created the MatchedSlot and MatchedIntent classes.

Then we discussed IRIS memory and how the session can be used to store attributes for the long term and the short term. We discussed the Session and SessionStorage classes.

We then discussed how a conversation can be modeled as a state machine problem. We discussed the different components of a state machine such as states, transitions, shields, and the StateMachine backbone class.

Then we discussed an insurance-focused use case capable of performing certain actions based on different intents and states. We defined various intents, slots, and slot types for the use case. We added these definitions to the configuration class.

We then discussed all the possible states for the use case and explained the execution part of all of these states. As some of the states require a validator before transition, we discussed shields that are required for our example use case. We briefly talked about the general query state and how to leverage an enterprise search in case the utterance is not classified into any of the explicit intents and does not match any document in the knowledge repository.

We then described possible transitions from one state to another depending on the user intent.

We then discussed StateMachineManager, which uses the configuration and performs intent matching before triggering state actions.

Lastly, we discussed how to make IRIS functional. We briefly explained how to expose IRIS as a REST service by the creation of service and controller layers.

In the next chapter, we will discuss the other chatbot frameworks available in the marketplace such as RASA, Google Dialogflow, and Microsoft Bot Framework. These frameworks, unlike our build-from-scratch approach, provide many plug-and-play features and make development faster. However, we recommend that you understand the requirements of your enterprise thorougly before making a choice between the available frameworks.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset