- Introduction
- Entity Event Handler Structure
- Writing Event Handler Functions
- Supported Events
- Access the Backend Using HTTP REST Calls
- Code Samples
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.
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.
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 typeEntityEventHandlerMetadata
. - The
handlers
method that returns an object of typeEntityEventHandlers
.
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
}
};
}
}
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.
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. |
|
publishMessage |
A generic fallback handler that is called when an item-specific prompt or a disambiguate handler is not specified. |
|
maxPromptsReached |
A generic fallback handler when an item-specific handler for reaching max prompts is not specified. |
|
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. |
|
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. |
|
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. |
|
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. |
|
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 |
|
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() . |
|
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() . |
|
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() . |
|
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');
}
}
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 |
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;
}
}
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.
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.
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});
}
}
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
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"));
}
}
}
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);
}
}
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.
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.