Skip to content

Latest commit

 

History

History
384 lines (314 loc) · 26 KB

ENTITY_EVENT_HANDLER.md

File metadata and controls

384 lines (314 loc) · 26 KB

Writing Entity Event Handlers

Table of contents

Introduction

An Entity Event Handler (EEH) enables the System.CommonResponse (CRC) and System.ResolveEntities (RE) components to resolve composite bag entities with events rather than with the FreeMarker expressions that you define in the Edit Bag Item screen. This event-driven approach ensures that the right code gets executed at the right time while the composite bag entity is getting resolved. It also enables you to control the messages that are sent to users, because you can replace or modify the messages generated by the CRC and RE components in the event handlers.

The EEH is deployed as part of a custom component service, but you don't have to write a state for it in the dialog flow to handle some type of background logic as you would for a custom component because it's instead registered to a composite bag entity. The CRC or RE component that handles this entity identifies a series of events. When one of these events occurs, the component first checks whether an EEH has been registered to the composite bag entity. If so, it then checks if the EEH has a handler defined for that event. When such a handler exists, the component invokes that event handler method in the EEH.

After the composite bag entity has been fully resolved, the 'resolved' event is fired to allow backend calls in the EEH.

While you can continue to use FreeMarker expression when defining a composite bag entity, you can avoid writing them altogether by writing event handlers in JavaScript or Typescript.

Entity Event Handler Structure

Using Javascript

An entity event handler exports two objects: the metadata object that provides the name of the component and the eventHandlerType (of which only the ResolveEntities type is supported), and the handlers object that contains the various entity-level, item-level, and custom-level event handler functions.

module.exports = {
  metadata: {
    name: 'myEntityEventHandler',
    eventHandlerType: 'ResolveEntities',
    supportedActions: [] // string array of transition actions that might be set by the event handler
  },
  handlers:  {
    entity: {
      resolved:async (event, context) => {
        // logic to execute once entity is resolved goes here
      }
      // more entity-level handlers here
    },
    items: {
      someCompositeBagItemName: {
        // item-level handlers here
      }
    },
    custom: {
      // custom handlers here
    }
  }
}

The metadata and handlers members can be defined as a function rather than as an object, if needed.

Using TypeScript

In TypeScript, the event handler class implements the EntityEventHandler interface. This interface requires both of the following methods:

  • The metadata method that returns an object of type EntityEventHandlerMetadata.
  • The handlers method that returns an object of type EntityEventHandlers.
import { EntityResolutionContext, EntityEventHandler, EntityEventHandlers, EntityEventHandlerMetadata, EntityEvent} from '@oracle/bots-node-sdk/lib';

export class MyEntityEventHandler implements EntityEventHandler {

  public metadata(): EntityEventHandlerMetadata {
    return { 
      name: 'myEntityEventHandler',    
      eventHandlerType: 'ResolveEntities',
      supportedActions: [] // string array of transition actions that might be set by the event handler
      };
  }

  public handlers(): EntityEventHandlers {
    return {
      entity: {
        resolved:async (event: EntityEvent, context: EntityResolutionContext) => {
          // logic to execute once entity is resolved goes here
        }
        // more entity-level handlers here
      },
      items: {
        someCompositeBagItemName: {
          // item-level handlers here
        }
      },
      custom: {
        // custom handlers here
      }
    };
  }

} 

Writing Event Handler Functions

The first argument of each event method is the event object. The properties available in this object depend on the type of event. See the list of supported entity events for information on which properties are available with which event.

The second argument of each event method is the context object. This object references the EntityResolutionContext that provides access to many convenience methods used to create your event handler logic.

You can find more information on creating conversation messages from an event handler here.

TIP: if you are using a JavaScript IDE like Visual Studio Code, you can get code insight and code completion support by defining the types used in the event handlers as follows in your JavaScript handler file:

const { EntityResolutionContext, EntityEventHandler, EntityEventHandlers, EntityEventHandlerMetadata, EntityEvent } = require ('@oracle/bots-node-sdk/lib');

When using TypeScript you will automatically get code completion support when your IDE supports it.

Supported Events

Entity Level Events

The table below lists all entity level events currently supported:

Event Description Event Properties
init A handler that can be used for initialization tasks. It is called when the entity resolution process starts. none
validate A handler for entity-level validations that include two or more bag item values. Validation errors can be registered by calling context.addValidationError(itemName,errorMessage). The handler returns false if the validation fails. None of the item values provided by the last user message will be updated when the validation fails.

NOTE: This handler is called when at least one bag item value has changed.
  • newValues: JSONObject with key-value pairs where the key is the item name and the value is the new item value.
  • oldValues: JSONObject with key-value pairs where the key is the item name and the value the old item value.
  • currentItem: The name of the item that's currently being resolved.
publishMessage A generic fallback handler that is called when an item-specific prompt or a disambiguate handler is not specified.
  • currentItem: The name of the item that's currently being resolved.
  • promptCount: The number of times that the user is prompted for the current item (only set for a prompt event).
  • disambiguationValues: A list of values that matches the user input (only set for a disambiguate event).
maxPromptsReached A generic fallback handler when an item-specific handler for reaching max prompts is not specified.
  • currentItem: The name of item that's currently being resolved.
  • promptCount: The number of times that the user is prompted for the current item.
resolved A function that gets called when the composite bag entity is resolved. You will typically use this function to call some backend API to complete the transaction that the composite bag entity has collected the data for. If the backend API call returns errors, possibly forcing a re-prompting for some invalid bag items, you can enable the re-prompting by simply clearing those bag items. The System.CommonResponse and System.ResolveEntities components will notice that the entity is not fully resolved after all, and will resume prompting for the missing bag items. none
attachmentReceived A function that gets called when the user sends an attachment. If the attachment can be mapped to a composite bag item, the validate function of that item will be called first.
  • value: The attachment JSON object with type and url properties.
locationReceived A function that gets called when the user sends a location. If the location can be mapped to a composite bag item, the validate function of that item will be called first.
  • value: The location JSON object with latitude and longitude properties.
disambiguateBagItem A handler that can be used to modify the message that is shown to disambiguate between bag items when an entity match applies to more than one bag item. Note that this handler only fires when the skill configuration parameter Use Enhanced Slot Filling is switched on.
  • matchValue: The entity value matched based on the user input
  • matchedBagItems: list of the names of the bag items that are matched against the entity value.
  • userInput: the last user input message that matched to multiple bag items.
userInputReceived A handler that can be used to inspect and modify new item matches and disambiguation values before the bag items are validated and updated. The handler is invoked on every turn while resolving the composite bag entity.
  • currentItem: The full name of the bag item currently being resolved.
  • userInput: the last user input message.
  • newItemMatches: a key-value map where each key is the full name of a bag item, and the value the candidate value for the item. The new item matches can be changed using context.setItemMatches and context.clearItemMatch.
  • disambiguationValues: a key-value map where each key is the full name of a bag item, and the value a list of matched values for the item. The disambiguation values for an item can be changed using context.setDisambiguationValues() or context.clearDisambiguationValues()
  • disambiguationItems: a key-value map where each key is the full name of a bag item, and the value a map with two properties:
    • matchValue: an entity value that matches against multiple bag items
    • matchedBagItems: list of the names of all the bag items that match against the entity value. The first item in the list is used as the key in the disambiguationItems map.
    A disambiguation value that matches multiple items can be removed by calling context.clearDisambiguationItems() and passing the full name of the first item in the list as argument.

Entity-Item Level Events

The table below lists all entity-item level events currently supported:

Event Description Event Properties
shouldPrompt A function that should return a boolean to indicate whether the System.ResolveEntities or System.CommonResponse components should prompt the user for a value for this item. This handler can be used to conditionally prompt for an item based on the values of other items in the bag. This handler takes precedence over the Prompt for Value field in the Edit Bag Item screen. If no value is returned, this is considered as a false return value, and the user will not be prompted for the item. none
validate A handler for item-level validations. Item validation errors can be registered by calling context.addValidationError(itemName,errorMessage). These validations are in addition to the validations specified using FreeMarker in the Edit Bag Item screen. If a FreeMarker validation has already failed for the item, then the validate event handler is not called. If validation should fail, this handler should return false.

NOTE 1: This handler is only called when the item value is set or updated. If the validity also depends on other bag item values, then the validation rule should be implemented in the entity-level validate handler.

NOTE 2: Both this item-level validate handler and the entity-level validate handler must return true for the new item value to be stored on the composite bag entity
  • currentItem: The name of item that's currently being resolved.
  • oldValue: The old valid value of the bag item (not present when the value is not set).
  • newValue: The new value that's entered by the user that must be validated.
publishPromptMessage A function that can be used to replace or extend the skill message generated by the System.ResolveEntities and System.CommonResponse to prompt for the item. These components take the prompt from the prompts registered in the Edit Bag Item screen. To use the messages generated by the System.ResolveEntities or System.CommonResponse components, you can call context.addCandidateMessages().
  • promptCount: The number of times that the user is prompted for current item.
publishDisambiguateMessage A function that can be used to replace or extend the skill message generated by the System.ResolveEntities or System.CommonResponse components to disambiguate the user-supplied item values. These components take the prompt from the disambiguation prompt registered in the Edit Bag Item screen. To use the messages generated by these components, you can call context.addCandidateMessages().
  • disambiguationValues: list of values that matches the user input.
maxPromptsReached A function that gets called when the maximum number of prompts for this item as specified in the Edit Bag Item screen has been reached. You can either skip this item by calling context.skipItem(itemName) and proceed with the next item in the bag, or you can abandon entity resolution by transitioning out of the component using context.cancel().
  • promptCount: number of times the user is prompted for current item.

Access the Backend Using HTTP REST Calls

There are several Node.js libraries that make HTTP requests easy. The list of these libraries changes frequently. You should review the pros and cons of the currently available libraries and decide which one works best for you. We recommend you use a library that supports JavaScript Promises so that you can leverage the async nature of the event handler methods to write your REST calls in a synchronous way. An easy choice might be the node fetch API that is pre-installed with the 'bots-node-sdk'. Use the following statement if you want to make REST calls using node-fetch:

const fetch = require("node-fetch");

or when using typescript:

import * as fetch from 'node-fetch';

The code to make REST calls with node fetch within an event handler looks like this:

  // Make a REST GET request
  const response = await fetch('http://some-backend-server-url');
  if (response.status === 200) {        
    const data = await response.json();
    // Do something with the data...
  } 

  // Make a REST POST request
  let payload = ...
  const response = await fetch('http://some-backend-server-url',{ method: 'POST', body: payload});
  if (response.status === 200) {        
    context.addMessage('Transaction successful');
  } else {
    context.addMessage('Transaction failed');
  }
}

Code Samples

The following samples use a composite bag entity called Expense, which has the following items:

Name Type Entity Name
Amount Entity CURRENCY
Date Entity DATE
Receipt Attachment
Type Entity ExpenseType

How to Conditionally Prompt for an Item

In this example, we only want the skill to prompt for a receipt when the expense amount is greater than 25 dollars. To do this, add the shouldPrompt event handler to conditionally prompt for an item. In the following snippet, this handller is added to the Amount item (a CURRENCY entity) to prompt for a receipt when the expense amount is greater than $25. The Amount item returns a CURRENCY JSON object ({ "amount":50, "currency":"dollar", "total_currency":"50.0 dollar", "entityName":"CURRENCY" }, for example). Its amount property holds the amount value.

items: {
  Receipt: {
    shouldPrompt:async (event, context) => {
      return context.getItemValue('Amount').amount > 25;
    }
  }

How to Validate an Item

Here is the event handler to enforce that the expense amount is at least 5:

items: {
  Amount: { 
    validate:async (event, context) => {
      let amount = event.newValue.amount;
      if (amount < 5) {
        context.addValidationError("Amount",`Amounts below 5 ${event.newValue.currency} cannot be expensed. Enter a higher amount or type 'cancel'.`);
      }
    }
  }

Note how we obtain the new value from the validation event object. Because the Amount item value is a CURRENCY JSON object, we need to obtain the amount property from it. We use the addValidationError function to register the error message. We could also have used conversation.addMessage(). The difference is that when using conversation.addMessage(), there is an optional second boolean argument keepProcessing that allows you to stop further processing in the System.CommonResponse and System.ResolveEntities components.

Instead of using hardcoded text to read the validation error message from the skill resource bundle, use the translate function instead:

items: {
  Amount: { 
    validate:async (event, context) => {
      let amount = event.newValue.amount;
      if (amount < 5) {
        context.addValidationError('Amount', context.translate('expense.amount.minimum', event.newValue.currency));
      }
    }
  }

For this code to work, your dialog flow definition must define the resource bundle as a variable named rb, and the resource bundle must contain the expense.amount.minimum key.

How to send acknowledgement messages for updated items or items resolved out of order

It's quite common for users to correct values that they entered previously, or to provide an item value that's required but has not yet been prompted for. In such cases, send the user an acknowledgement that the information provided has been understood and processed. You can implement this by first creating two helper functions that use the getItemsUpdated() and getItemsMatchedOutOfOrder() convenience methods of the entity resolution context:

function updatedItemsMessage(context) {
  if (context.getItemsUpdated().length>0) {
    let message = "I have updated"+context.getItemsUpdated().map((item, i) => (i!==0 ? " and the " : " the ")+item.toLowerCase()+" to "+context.getDisplayValue(item));
    context.addMessage(message);
  }
}
 
function outOfOrderItemsMessage(context) {
  if (context.getItemsMatchedOutOfOrder().length>0) {
    let message = "I got"+context.getItemsMatchedOutOfOrder().map((item, i) => (i!==0 ? " and the " : " the ")+item.toLowerCase()+" "+context.getDisplayValue(item));
    context.addMessage(message);
  }
}

Then call these functions from the generic publishMessage event handler that is called for any message that a System.CommonResponse or System.ResolveEntities component wants to publish when no item-specific event handler exists:

entity: {
  publishMessage:async (event, context) => {
    updatedItemsMessage(context, conversation);  
    outOfOrderItemsMessage(context, conversation);  
    context.addCandidateMessages();
  }

Remember that when you add an item-specific handler to publish a prompt or disambiguation message, you should call the same two functions in that handler if you want to preserve this acknowledgement functionality.

How to Set or Update Bag Items

Here is a example of updating the expense date and amount based on the scanned receipt.

items: {
  Receipt: {
    validate:async (event, context) => {
      if (event.newValue.type==='image') {
        if (event.newValue.url==='https://upload.wikimedia.org/wikipedia/commons/0/0b/ReceiptSwiss.jpg') {
          let amount =  {"entityName": "CURRENCY", "amount": 54.5,"currency":"chf","totalCurrency": "CHF 54.50 scanned from receipt"};
          let date =  {"entityName": "DATE", "date": 1185753600000,"originalString": "30 july 2007 scanned from receipt"};
          context.setItemValue("Amount",amount);
          context.setItemValue("Date",date);
          context.addMessage(`Receipt scanned, amount set to CHF 54.50 and date set to 30 july 2007`);
        }
      } else {
        context.addValidationError("Receipt",Receipt must be an image, cannot be ${event.newValue.type});
      }
    }

How to Create a Summary Message

When a composite bag entity is resolved, you typically want to show a summary of the the composite bag item values before proceeding. The getDisplayValues() method on the entity resolution context makes this quite easy. This method return a list of name-value pairs of each composite bag item. Using the reduce function you can easily construct a summary message like this:

resolved: async (event, context) => {
  let msg = 'Got it. Here is a summary of your expense:';
  msg += context.getDisplayValues().reduce((acc, curr) => `${acc}\n${curr.name}: ${curr.value}`, '');
  context.addMessage(msg);
} 

The summary message will look something like this:

Got it. Here is a summary of your expense:
Type: Taxi
Date: Thu Dec 22 2022
Amount: 20 dollar

How to Use Custom Events

The following example is for the use case where the expense date and expense amount are taken from the scanned receipt that's uploaded by the user. If the user then tries to change the date or the amount, the skill replies by telling him that the date or amount cannot be changed because they need to match the data on the receipt. The skill then provides the user an option to remove the receipt again so he can once more change the date. To implement the removal of the receipt, a custom event, removeReceipt, is invoked when the user taps the 'Yes' button:

items: {
  Date: {
    validate:async (event, context) => {
      if (context.getItemValue("Receipt")!==undefined && event.oldValue("Date")!==undefined) {

        const mf = context.getMessageFactory();
        const message = mf.createTextMessage('You cannot change the date that is extracted from the scanned receipt. Do you want to remove the receipt and change the date?')
          .addAction(mf.createPostbackAction('Yes', {"event" : {"name":"removeReceipt","properties":{"Date":event.value}}}))
          .addAction(mf.createPostbackAction('No', {}));
        context.addMessage(message, true);
        return false;
      }
      return true;
    }
  }
},
custom: {
  removeReceipt:async (event, context) => {
    context.clearItemValue("Receipt");
    if (event.Date) {
      context.setItemValue("Date", event.Date);
      context.addMessage("Receipt removed, date set to "+context.getDisplayValue("Date"));
    }
  }
}

How to Invoke a REST API

Calling a REST API from within an event handler is straightforward. Since all event handlers are async, you can use the await keyword in combination with an NPM HTTP request module that supports JavaScript Promises, like node-fetch. This allows you to write your asynchronous code in a synchronous matter.

entity: {
  resolved:async (event, context) => {
    try {
      let payload = context.getEntity();
      // do some transformations on entity JSON payload if needed...
      const response = await fetch('http://expense-backend-server/expense',{ method: 'POST', body: payload});
      if (response.status === 200) {
          context.addMessage(`Thank you for submitting your ${context.getItemValue('Type')} expense`);
      } 
    } catch (error) {
      context.logger().info("Error invoking API: "+error);
    }
  }

How to Send Rich Conversation Messages

As you have seen in the previous examples, you can use context.addMessage(<payload>) to create a bot message that is sent to the user. You can call this function multiple times to send multiple messages. See the section on Conversation Messaging for code samples on how to create the various message types, like text, card, attachment, table and (editable) form messages.

How to Modify Candidate Messages

In the publishXXX event handlers, you can receive the list of candidate messages created by the component by calling context.getCandidateMessageList(). This method returns a class representation of every message type, allowing you to use various methods in the class and in the MessageFactory to modify the message. You can then use context.addMessage(<payload>) to send the message. Here is an example:

const mf = context.getMessageFactory();
let msg = context.getCandidateMessageList()[0];
msg.addAction(mf.createPostbackAction('Go Foo',{'action': 'foo'}));
context.addMessage(msg);

If you are using TypeScript you can cast the message to the appropriate subclass of NonRawMessage to get design-time validation and code completion.