/* eslint-disable no-prototype-builtins */
'use strict';
const { BaseContext } = require("../component/baseContext");
const EventHandlerRequestSchemaFactory = require("./schema/eventHandlerRequestSchema");
const PARENT_SEPARATOR = '-';
// Response template
const RESPONSE = {
context: undefined,
error: false,
validationResults: {},
keepProcessing: true,
cancel: false,
modifyContext: false
};
/**
* The Bots EntityResolutionContext is a class for querying, validating and changing a composite bag entity and its
* entity resolution status.
* </p>
* An EntityResolutionContext class instance is passed as an argument to every event handler function.
* @memberof module:Lib
* @extends BaseContext
* @alias EntityResolutionContext
*/
class EntityResolutionContext extends BaseContext {
/**
* Constructor of entity resolution context.
* DO NOT USE - INSTANCE IS ALREADY PASSED TO EVENT HANDLERS
* @param {object} request
*/
constructor(request) {
// Initilize the response
const response = Object.assign({}, RESPONSE, {
entityResolutionStatus: request.entityResolutionStatus,
});
super(request, response, EventHandlerRequestSchemaFactory);
this._entityStatus = response.entityResolutionStatus;
this._entity = this.getVariable(request.variableName);
// Initialize display properties
this._initSystemEntityDisplayProperties();
}
/**
* Returns the value of the composite bag entity currently being resolved
* @return {object} The JSON object holding the composite bag item values
*/
getEntity() {
return this._entity;
}
/**
* Sets the value of the composite bag entity currently being resolved
* @param {object} newEntity - The JSON object holding the composite bag item values
*/
setEntity(newEntity) {
this._entity = newEntity;
delete this._entityStatus.resolvingField;
this.setVariable(this.getRequest().variableName,this._entity);
this._clearShouldPromptCache();
}
/**
* Returns the name of the composite bag entity type currently being resolved
* @return {string} name of the composite bag entity type
*/
getEntityName() {
let cbvarDef = this.getVariableDefinition(this.getRequest().variableName);
return cbvarDef ? cbvarDef.type.name : undefined;
}
/**
* Returns list of top-level composite bag item definitions.
* Nested bag items can be retrieved by using the "children" property of a parent bag item.
* @return {object[]} list of composite bag item definitions
*/
getEntityItems() {
let cbvarDef = this.getVariableDefinition(this.getRequest().variableName);
return cbvarDef ? cbvarDef.type.compositeBagItems : [];
}
/**
* Returns composite bag item definition for the (nested) bag item name
* @param {string} fullName - the full name of the (nested) composite bag item for which the value is returned
* @return {object} composite bag item definition
*/
getEntityItem(fullName) {
let names = fullName.split(PARENT_SEPARATOR);
let item = names.reduce( (curItem, name) => curItem && curItem.children ? curItem.children.find(c => c.name === name) : undefined ,{"children" : this.getEntityItems()});
if (!item) {
this.logger().error(`No bag item found with name ${fullName}`);
}
return item;
}
/**
* Return value of a composite bag item in the composite bag entity currentyly being resolved
* @return {object} value of the composite bag item
* @param {string} fullName - the full name of the (nested) composite bag item for which the value is returned
*/
getItemValue(fullName) {
let names = fullName.split(PARENT_SEPARATOR);
return names.reduce( (entityValue, name) => entityValue ? entityValue[name] : undefined , this._entity);
}
/**
* Set value of a (nested) composite bag item in the composite bag entity currentyly being resolved
* @param {string} fullName - the full name of the composite bag item for which the value is set
* @param {object} value - value of the composite bag item
*/
setItemValue(fullName, value) {
// init root entity if needed
if (!this._entity) {
this._entity = {"entityName": this.getEntityName()}
this.setVariable(this.getRequest().variableName,this._entity);
}
// created nested entity values if needed before setting nested bag item value
let entityValue = this._entity;
let names = fullName.split(PARENT_SEPARATOR);
let itemName = names.pop();
if (names.length > 0) {
// get or create nested entity values before setting nested bag item value
entityValue = names.reduce( (entityValue, name, index) => {
if (!entityValue[name]) {
// create parent entity value, need to lookup the corresponsing nested bag entity definition to set proper entityName and subType
let parentItemName = names.splice(index).join(PARENT_SEPARATOR);
let itemDef = this.getEntityItem(parentItemName);
let parent = {"entityName": itemDef.entityName};
if (itemDef.namedEntitySubType) {
parent.subType = itemDef.namedEntitySubType;
}
entityValue[name] = parent;
}
return entityValue[name];
}, entityValue);
}
entityValue[itemName] = value;
this._clearShouldPromptCache();
this.clearDisambiguationValues(fullName);
}
/**
* Remove the value of a composite bag item from the composite bag entity JSON object
* @param {string} fullName - full name of the composite bag item
*/
clearItemValue(fullName) {
let names = fullName.split(PARENT_SEPARATOR);
let entityValue = this._entity;
let itemName = names.pop();
if (names.length > 0) {
// get the nested entity value that holds the item we need to clear
entityValue = this.getItemValue(names.join(PARENT_SEPARATOR));
}
if (entityValue) {
delete entityValue[itemName];
}
this._clearShouldPromptCache();
}
/**
* Add a validation error for a composite bag item. This marks the item invalid and the
* the item will not be set/updated with the new invalid value. The error mesage will be
* published as bot message to the user.
* @param {string} itemName - name of composite bag iten that validation error applies to
* @param {string} error - the error message
*/
addValidationError(itemName, error) {
this._entityStatus.validationErrors[itemName] = error;
}
/**
* Returns validation errors
* @return {object} validation errors keyed by item name
*/
getValidationErrors() {
return this._entityStatus.validationErrors;
}
/**
* Returns the disambiguation values that are found based on the last user input for a specific bag item
* @return {object[]} the disambiguations values. This is a string array for bag items that have a custom
* entity type, and a JSONObject array for bag items with a system entity type
* @param {string} itemName - name of the composite bag item
*/
getDisambiguationValues(itemName) {
return this._entityStatus.disambiguationValues[itemName] || [];
}
/**
* Sets the disambiguation values for a specific bag item
* @param {string} itemName - name of the composite bag item
* @param {object[]} disambiguationValues - this is a string array for bag items that have a custom
* entity type, and a JSONObject array for bag items with a system entity type
*/
setDisambiguationValues(itemName, disambiguationValues) {
this._entityStatus.disambiguationValues[itemName] = disambiguationValues;
}
/**
* Removes the disambiguation values that are found based on the last user input for a specific bag item
* @param {string} itemName - name of the composite bag item, if not specified, all disambiguation values
* of all items will be cleared
*/
clearDisambiguationValues(itemName) {
if (itemName) {
delete this._entityStatus.disambiguationValues[itemName];
} else {
// clear all disambiguation values
this._entityStatus.disambiguationValues = {};
}
}
/**
* Removes the disambiguation items that are matched for a single entity value using the last user input.
* @param {string} itemName - full name of the first composite bag item that matches the entity value,
* if not specified, all disambiguation items will be cleared
*/
clearDisambiguationItems(itemName) {
if (itemName) {
delete this._entityStatus.disambiguationMatches[itemName];
} else {
// clear all disambiguation values
this._entityStatus.disambiguationMatches = {};
}
}
/**
* Returns the name of the bag item that is currently being resolved
* @return {string} the bag item name
*/
getCurrentItem() {
return this._entityStatus.resolvingField;
}
/**
* Returns the last user input message. If the last message was not a text message, this function returns undefined
* @return {string} the user text message
*/
getUserInput() {
return this._entityStatus.userInput;
}
/**
* Returns boolean flag indicating whether the component used to resolve the composite bag entity
* (System.ResolveEntities or System.CommonResponse) has set the useFullEntityMatches property to true.
* When set to true, custom entity values are stored as JSON object, similar to the builtin entities
* that are always stored as JSON object.
*
* @return {boolean} fullEntityMatches flag
*/
isFullEntityMatches() {
return this._entityStatus.useFullEntityMatches;
}
/**
* Mark a composite bag item as skipped, which means the ResolveEntities or CommonResponse component
* will no longer prompt for a value for the bag item
* @param {string} name - full name of the composite bag item
*/
skipItem(name) {
this._entityStatus.skippedItems.push(name);
//clear resolving field if set to item that is now being skipped
if (name === this._entityStatus.resolvingField) {
delete this._entityStatus.resolvingField;
}
}
/**
* Unmark a composite bag item as skipped, which means the ResolveEntities or CommonResponse component
* will prompt again for a value for the bag item
* @param {string} name - full name of the composite bag item
*/
unskipItem(name) {
this._entityStatus.skippedItems = this._entityStatus.skippedItems.filter(item => item !== name);
}
/**
* Returns true when item is marked as skipped, returns false otherwise
* @return {boolean} skip item flag
* @param {string} name - full name of the composite bag item
*/
isSkippedItem(name) {
return this._entityStatus.skippedItems.includes(name);
}
/**
* Returns a list of the candidate bot messages created by the the ResolveEntities or CommonResponse component
* that will be sent to the user when you use addCandidateMessages() function.
* @return {NonRawMessagePayload[]} list of candidate messages. The messages are returned in the JSON format of the conversation
* message model (CMM).
* @deprecated Use getCandidateMessageList instead
*/
getCandidateMessages() {
return this.getRequest().candidateMessages;
}
/**
* Returns a list of the candidate bot messages created by the the ResolveEntities or CommonResponse component
* that will be sent to the user when you use addCandidateMessages() function.
* @returns {NonRawMessage[]} list of candidate messages. The messages are returned in the class representation of
* each message type. You can modify it using the available class methods, and you can add the message by
* calling context.addMessage().
* <p>
* See [Conversation Messaging]{@link https://github.com/oracle/bots-node-sdk/blob/master/MESSAGE_FACTORY.md}
*/
getCandidateMessageList() {
const mf = this.getMessageFactory();
return this.getRequest().candidateMessages.map(msg => mf.messageFromJson(msg));
}
/**
* Add the bot messages created by ResolveEntities or CommomResponse component to the response that will
* be sent to the user.
* Note that these messages are in the format of the conversation message model (CMM).
*/
addCandidateMessages() {
if (this.getRequest().candidateMessages) {
if (!this.getResponse().messages) {
this.getResponse().messages = [];
}
this._logger.debug("Using candidate bot messages");
for (let message of this.getRequest().candidateMessages) {
this.getResponse().messages.push(message);
}
this.getResponse().keepProcessing = false;
} else {
this._logger.debug("No candidate bot messages found");
}
}
/**
* Returns the list of messages that will be sent to the user
* @return list of messages
*/
getMessages() {
return this.getResponse().messages || [];
}
/**
* Returns the list of messages that will be sent to the user
* @returns {NonRawMessage[]} list of messages, returned in the class representation of each message type.
*/
getMessageList() {
const messages = this.getResponse().messages || [];
const mf = this.getMessageFactory();
return messages.map(msg => mf.messageFromJson(msg));
}
/**
* Adds a message to the bot response sent to the user.
* @param {object} payload - can take a string message, a message created using MessageFactory, or a message created using
* the deprecated MessageModel.
* @param {boolean} [keepProcessing] - If set to false (the default), the message will be sent to the user and
* the ResolveEntities or CommonResponse component will stop any further processing, and wait for user input.
* If set to true, the component will continue processing, possibly sending more messages to the
* user before releasing the turn
*/
addMessage(payload, keepProcessing) {
this.getResponse().keepProcessing = !!keepProcessing;
this.getResponse().messages = this.getResponse().messages || [];
this.getResponse().messages.push(super.constructMessagePayload(payload));
}
/**
* Returns the composite bag item definitions that already had a value and have gotten a new value
* extracted from the last user input.
* @return {string[]} list of composite bag item definitions
*/
getItemDefsUpdated() {
return this._entityStatus.updatedEntities;
}
/**
* Returns the composite bag item (full) names that already had a value and have gotten a new value
* extracted from the last user input.
* @return {string[]} list of composite bag item full names
* @deprecated use getItemDefsUpdated instead which returns the complete item definition instead of just the full name
*/
getItemsUpdated() {
return this._entityStatus.updatedEntities.map(ent => ent.fullName || ent.name);
}
/**
* Returns the composite bag item definitions that have gotten a new value
* extracted from the last user input while the user was prompted for
* another bag item.
* @return {string[]} list of composite bag item definitions
*/
getItemDefsMatchedOutOfOrder() {
return this._entityStatus.outOfOrderMatches;
}
/**
* Returns the composite bag item (fulll) names that have gotten a new value
* extracted from the last user input while the user was prompted for
* another bag item.
* @return {string[]} list of composite bag item full names
* @deprecated use getItemDefsMatchedOutOfOrder instead which returns the complete item definition instead of just the full name
*/
getItemsMatchedOutOfOrder() {
return this._entityStatus.outOfOrderMatches.map(ent => ent.fullName || ent.name);
}
/**
* Returns the composite bag item definitions that have gotten a new value
* extracted from the last user input
* @return {string[]} list of composite bag item definitions
*/
getItemDefsMatched() {
return this._entityStatus.allMatches;
}
/**
* Returns the composite bag item (full) names) that have gotten a new value
* extracted from the last user input
* @return {string[]} list of composite bag item full names
* @deprecated use getItemDefsMatched instead which returns the complete item definition instead of just the full name
*/
getItemsMatched() {
return this._entityStatus.allMatches.map(ent => ent.fullName || ent.name);
}
/**
* Returns list of enumeration values for the bag item that is currently being resolved.
* This list is paginated, it only includes the values in current range
* @return {object[]} list of enumeration values
*/
getEnumValues() {
return this._entityStatus.enumValues;
}
/**
* A bag item of type system entity, LOCATION and ATTACHMENT has a JSON Object as value.
* With this function you can override the default display properties of the JSON
* Object that should be used to print out a string representation of the value.
* @param {string} entityName - name of the system entity, or 'ATTACHMENT' or 'LOCATION'.
* For an entity with a subtype, you need to include the subtype separated by a dot, for example DATE_TIME.INTERVAL.
* @param {string[]} properties - array of property names
*/
setSystemEntityDisplayProperties(entityName, properties) {
this._systemEntityDisplayProperties[entityName].properties=properties;
}
/**
* A bag item of type system entity, LOCATION and ATTACHMENT has a JSON Object as value.
* With this function you can override the default display function that is applied to the
* display property values. The function is called with each display property as an argument
* For example, this is the default display function for DURATION:
* ((startDate,endDate) => new Date(startDate)+" - "+new Date(endDate))
* If you want to format the dates differently, you can use a library like moments.js
* and call this function to override the display function
* Object that should be used to print out a string representation of the value.
* @param {string} entityName - name of the system entity, or 'ATTACHMENT' or 'LOCATION'
* For an entity with a subtype, you need to include the subtype separated by a dot, for example DATE_TIME.INTERVAL.
* @param {object} displayFunction - the display function applied to the display properties
*/
setSystemEntityDisplayFunction(entityName, displayFunction) {
this._systemEntityDisplayProperties[entityName].function=displayFunction;
}
/**
* Returns the display value for a composite bag item.
* For bag items with a custom entity type, the display value returned is the value property of the
* JSON Object value when isFullEntityMatches returns true. When isFullEntityMatches returns false, the actual value is returned.
* For STRING bag item types, the display value is the same as the actual value.
* For system entities, and for bag item types LOCATION and ATTACHMENT the configured display
* properties and display function determine the display value
* @see isFullEntityMatches
* @see setSystemEntityDisplayProperties
* @see setSystemEntityDisplayFunction
* @return {string} display value of composite bag item
* @param {string} itemName - full name of the composite bag item
*/
getDisplayValue(itemName) {
let itemValue = this.getItemValue(itemName);
let item = this.getEntityItem(itemName);
if (item) {
// bag item types ATTACHMENT and LOCATION also have display properties
// For entities that have a subType like DATE_TIME, the subType is added to the name, separated by a dot
let entityName = item.entityName ? (item.namedEntitySubType ? item.entityName + '.' +item.namedEntitySubType : item.entityName ) : item.type+"_ITEM";
if (entityName === 'DATE_TIME.RECURRING') {
itemValue = this._getDateTimeRecurringDisplayValue(item, itemValue);
} else {
itemValue = this._getDisplayValue(entityName, itemValue);
}
}
return itemValue;
}
/**
* Returns the display values for a composite bag entity.
* @see getDisplayValue
* @see setSystemEntityDisplayProperties
* @see setSystemEntityDisplayFunction
* @return {object[]} list of display values of all bag items in the composite bag entity. Each display value is an object with two properties, the name and the value.
* @param {string} itemNames - you can specify one or more item names as argument. If you do this, only the display
* values of these items will be returned. If you do not specify an item name, the display values of all
* items in the bag will be returned.
*/
getDisplayValues() {
// convert arguments to real array so we can use includes function
let args = Array.prototype.slice.call(arguments);
let itemValues = [];
for (let item of this.getEntityItems()) {
if (this._entity.hasOwnProperty(item.name) && (args.length===0 || args.includes(item.name))) {
let itemValue = this.getDisplayValue(item.name);
itemValues.push({name: item.label || item.name, value: itemValue});
}
}
return itemValues;
}
/**
* Cancels the entity resolution process and sets the 'cancel' transition on the ResolveEntities or Common Response component.
*/
cancel() {
this.getResponse().cancel = true;
}
/**
* Set a transition action. When you use this function, the entity resolution process is aborted, and the dialog engine will transition
* to the state defined for this transition action.
* <p>
* NOTE: This method cannot be used in the init event handler
* @param {string} action - name of the transition action
*/
setTransitionAction(action) {
this.getResponse().transitionAction = action;
}
/**
* Sets the value of a custom property that is stored in the entity resolution context. A custom property can be
* used to maintain custom state accross event handler calls while resolving the composite bag entity.
* If you set the value to null, the custom property will be removed.
* @param {string} name - name of the custom property
* @param {object} value - value of the custom property
*/
setCustomProperty(name, value) {
if (value===null) {
delete this._entityStatus.customProperties[name];
} else {
this._entityStatus.customProperties[name] = value;
}
}
/**
* Returns the value of a custom property that is stored in the entity resolution context. A custom property can be
* used to maintain custom state accross event handler calls while resolving the composite bag entity.
* @return {object} value of the custom property
* @param {string} name - name of the custom property
*/
getCustomProperty(name) {
return this._entityStatus.customProperties[name];
}
/**
* Returns information about the entity resolution status
* @return {object} the status object
*/
getEntityResolutionStatus() {
return this._entityStatus;
}
/**
* Set the bag item matches. Note that this method only takes effect when invoked from the userInputReceived event handler.
* @param {map} matches - map where the key is the full item name and the value the item match object.
* @return {EntityResolutionStatus} the status object
*/
setItemMatches(matches) {
this._entityStatus.newItemMatches = matches;
}
/**
* Clear the entity match for a specific bag item. Note that this method only takes effect when invoked from the userInputReceived event handler.
* @param {string} name - the full item name for which the match needs to be removed.
* @return {EntityResolutionStatus} the status object
*/
clearItemMatch(name) {
delete this._entityStatus.newItemMatches[name];
}
/**
* Create display value for bag item of type DATE_TIME.RECURRING
* INTERNAL ONLY - DO NOT USE
* @private
*/
_getDateTimeRecurringDisplayValue(item, itemValue) {
let displayValue = '';
for (let childItem of item.children) {
let childValue = itemValue[childItem.name];
if (childValue) {
let label = childItem.label || childItem.name;
let subType = Array.isArray(childValue) ? childValue[0].subType : childValue.subType;
displayValue += '\n' + label + ': ' + this._getDisplayValue('DATE_TIME.'+subType, childValue);
}
}
return displayValue;
}
/**
* Configure default display properties for all system entities, and ATTACHMENT and LOCATION item types
* INTERNAL ONLY - DO NOT USE
* @private
*/
_initSystemEntityDisplayProperties() {
this._systemEntityDisplayProperties = {
"EMAIL": {"properties": ["email"]}
,"CURRENCY": {"properties": ["amount", "currency"]}
,"NUMBER": {"properties": ["number"]}
,"YES_NO": {"properties": ["yesno"]}
,"DATE": {"properties": ["date"], "function": (date => new Date(date).toDateString())}
,"TIME": {"properties": ["originalString"]}
,"DURATION": {"properties": ["startDate","endDate"], "function": ((startDate,endDate) => new Date(startDate).toDateString()+" - "+new Date(endDate).toDateString())}
,"ADDRESS": {"properties": ["originalString"]}
,"PERSON": {"properties": ["originalString"]}
,"PHONE_NUMBER": {"properties": ["completeNumber"]}
,"SET": {"properties": ["originalString"]}
,"URL": {"properties": ["fullPath"]}
,"ATTACHMENT_ITEM": {"properties": ["url"]}
,"LOCATION_ITEM": {"properties": ["latitude, longitude"]}
,"DATE_TIME.DATE": {"properties": ["value"], "function": (value => new Date(value).toDateString())}
,"DATE_TIME.TIME": {"properties": ["value"], "function": (value => value.substring(0,5))}
,"DATE_TIME.DURATION": {"properties": ["value"]}
,"DATE_TIME.DATETIME": {"properties": ["date", "time"], "function": ((date,time) => (date ? (new Date(date.value).toDateString() + ' ') : '') + (time ? time.value.substring(0,5) : ''))}
,"DATE_TIME.INTERVAL": {"properties": ["startDate", "startTime", "endDate", "endTime"], "function": ((startDate,startTime, endDate, endTime) => (startDate ? new Date(startDate.value).toDateString() + " " : "") + (startTime ? startTime.value.substring(0,5) : "") + ((((endDate && endDate.value !== (startDate ? startDate.value : '')) || endTime) && (startDate || startTime)) ? " - " : "") + (endDate && endDate.value !== (startDate ? startDate.value : '') ? (new Date(endDate.value).toDateString() + " ") : "") + (endTime ? endTime.value.substring(0,5) : ""))}
};
}
/**
* Returns display value for a composite bag item raw value using the display properties
* configured for the system entity
* INTERNAL ONLY - DO NOT USE - Use getDisplayValue(itemName) instead
* @return {string} the display value
* @param {string} entityName - name of the bag item entity type
* @param {object} rawValue - value of the bag item
* @private
*/
_getDisplayValue(entityName, rawValue) {
let props = this._systemEntityDisplayProperties[entityName];
let self = this;
let getValue = (itemValue) => {
if (props && props.properties) {
let args = props.properties.map(p => itemValue[p]);
if (props.hasOwnProperty('function')) {
return props['function'](...args);
} else {
return args.join(' ');
}
} else {
if (self.isFullEntityMatches() && itemValue && itemValue.hasOwnProperty('value')) {
return itemValue.value;
} else {
return itemValue;
}
}
};
if (Array.isArray(rawValue)) {
return rawValue.map(v => getValue(v)).join(', ');
} else {
return getValue(rawValue);
}
}
/**
* Clears the cache with information which items should be prompted for a value
* INTERNAL ONLY - DO NOT USE
* @private
*/
_clearShouldPromptCache() {
this._entityStatus.shouldPromptCache = {};
}
/**
* Returns the cache with information which items should be prompted for a value.
* @return {object} Cache is a JSON object with item names as key and a boolean value as value.
* INTERNAL ONLY - DO NOT USE
* @private
*/
_getShouldPromptCache() {
return this._entityStatus.shouldPromptCache;
}
}
module.exports = { EntityResolutionContext }