Update Dynamic Entity Values

Dynamic entities are special entities that you can update by calling the dynamic entity APIs. Here's how to use JavaScript to add, modify, and delete a dynamic entity's values from information that's in a JSON file.

Disclaimer: This example is provided as-is, with no support provided by Oracle world-wide customer support.

Create the Folder Structure and Add Files

Follow these steps to create the folder structure and add the necessary files.

  1. Create the top-level folder. For example: dynamic-entities-import.

  2. Add a package.json file with the following content:

    {
      "name": "dynamic-entities-import",
      "version": "2.0.0",
      "description": "This is the code for the dynamic entities import use case in the REST Reference.",
      "main": "main.js",
      "scripts": {
        "start": "node src/main.js",
        "start:dev": "nodemon --inspect src/main.js",
        "lint": "eslint .",
        "lint:fix": "eslint . --fix"
      },
      "keywords": [],
      "author": "",
      "dependencies": {
        "log4js": "^6.3.0",
        "oci-common": "^2.1.0"
      },
      "devDependencies": {
        "eslint": "^7.32.0",
        "nodemon": "^2.0.12"
      }
    }
    
  3. In the top-level folder, create config and src subfolders.

  4. Add a file named config.js to the config folder and add this content:

    /*
     * Logging level ALL|INFO|DEBUG|WARN|ERROR|FATAL|OFF|TRACE
     */
    module.exports.LOGGING_LEVEL = 'INFO';
    
    /*
     * OCI request signature configurations
     */
    
    // OCI CLI config file
    module.exports.OCI_CONFIG_FILE_PATH = '~/.oci/config';
    // OCI CLI profile name
    module.exports.OCI_CONFIG_PROFILE_NAME = 'DEFAULT';
    
    /*
     * Oracle Digital Assistant configurations
     */
    
    // Digital Assistant host name without "https://"
    module.exports.ODA_HOSTNAME= '';
    // API base path - do not modify
    module.exports.DYNAMIC_ENTITIES_API_BASE_PATH = '/api/v1';
    
    /*
     * Run configurations 
     */
    
    // Full path of the JSON file that contains the patch data
    module.exports.DYNAMIC_ENTITIES_PATCH_DATA_PATH = '~/patchData.json';
    
    // Name of the skill to upload the dynamic entities to
    module.exports.ODA_SKILL_NAME = '';
    // Version of the named skill
    module.exports.ODA_SKILL_VERSION = '';
    // Name of the dynamic entity to modify
    module.exports.ODA_ENTITY_NAME = '';
    // Set copy to true to retain the original value set
    module.exports.DYNAMIC_ENTITIES_COPY = true;
    
    /*
     * Constants
     */
    
    // Maximum number of tries to check push request status
    module.exports.MAX_STATUS_RETRIES = 50;
    // Active export task statuses - do not modify
    module.exports.ACTIVE_STATUSES= ['INPROGRESS', 'TRAINING'];
    
  5. In the src folder, create a file named main.js, and then add this code:

    const CONFIG = require('../config/config');
    const ODAManager = require('./lib/ODAManager');
    
    const log4js = require('log4js');
    const logger = log4js.getLogger('Dynamic Entities Importer');
    logger.level = CONFIG.LOGGING_LEVEL;
    
    // Start insights export job
    ODAManager.startDynamicEntityImport()
      .then( () => {logger.info('Dynamic entity updated.');})
      .catch(error => {
        logger.debug(error);
        logger.info('Could not update dynamic entity with patch data.');
      });
    
  6. Create a lib subfolder under src.

  7. Complete the instructions in the Send Signed Requests use case to add OCIManager.js to the lib directory, create the OCI config file, and update the package's config/config.js file to provide the OCI request signature configurations.

  8. In the lib folder, create ODAManager.js, and then add this content.

    const CONFIG = require('../../config/config');
    const OCIManager = require('./OCIManager');
    
    const os = require('os');
    const log4js = require('log4js');
    const logger = log4js.getLogger('ODA Manager');
    logger.level = CONFIG.LOGGING_LEVEL;
    
    class ODAManager {
      constructor() {
    
      }
    
      /**
       * Push patch data to dynamic entity
       */
      async startDynamicEntityImport() {
        let dataPath =
          (CONFIG.DYNAMIC_ENTITIES_PATCH_DATA_PATH.indexOf('~/') === 0) ? CONFIG.DYNAMIC_ENTITIES_PATCH_DATA_PATH.replace('~', os.homedir()) :
            CONFIG.DYNAMIC_ENTITIES_PATCH_DATA_PATH;
        try {
          // Make sure it's a valid path
          require(dataPath);
        } catch (error) {
          const errorMessage = `Can't open the patch data file ${CONFIG.DYNAMIC_ENTITIES_PATCH_DATA_PATH} or it isn't valid JSON`;
          logger.error(errorMessage);
          throw (error);
        }
        try {
          // Get bot ID for identified skill
          const botId = await this._lookupBotId(CONFIG.ODA_SKILL_NAME, CONFIG.ODA_SKILL_VERSION);
    
          // Get ID for named entity
          const entityId = await this._lookupEntityId(botId, CONFIG.ODA_ENTITY_NAME);
    
          // Make sure there are no active push requests for this entity
          const pushRequestsList = await this._getPushRequests(botId, entityId);
          await this._checkForActiveRequests(pushRequestsList);
    
          // Create the push request
          const pushRequestData = await this._createPushRequest(botId, entityId, (CONFIG.DYNAMIC_ENTITIES_COPY === undefined) ? false : CONFIG.DYNAMIC_ENTITIES_COPY);
    
          // Push the data
          const pushDataResponse = await this._pushDataToRequest(botId, entityId, pushRequestData.result.id, dataPath);
    
          // Wait for export job to finish
          await this._finalizePushRequest(botId, entityId, pushRequestData.result.id);
          const result = await this._trackStatus(botId, entityId, pushRequestData.result.id);
    
          // Conclusion
          if (CONFIG.ACTIVE_STATUSES.includes(result)) {
            logger.info(`The request job still hasn't completed. Current status is ${result}. Check job status later.`);
          } else {
            // Report the results
            logger.info(`The push request job has finished. Status = ${result}.`);
            if (result === 'COMPLETED') {
              logger.info(`Total deleted: ${pushDataResponse.result.totalDeleted}`);
              logger.info(`Total added: ${pushDataResponse.result.totalAdded}`);
              logger.info(`Total modified: ${pushDataResponse.result.totalModified}`);
            }
          }
        } catch (error) {
          const errorMessage = `Error updating dynamic entity. Detailed error: ${error.message} Request ID: ${error.opcRequestId}`;
          logger.error(errorMessage);
          throw (error);
        }
      }
    
      /**
       * Stop if there's another push request in process.
       *
       * @param pushRequestsList - Results from
       *   GET /api/v1/bots/{botId}/dynamicEntities/{entityId}/pushRequests
       * @returns {object} - pushRequestsList
        */
      async _checkForActiveRequests(pushRequestsList) {
        return new Promise(function (resolve, reject) {
          const activeRequests = pushRequestsList.result.filter(val => CONFIG.ACTIVE_STATUSES.includes(val.status.toUpperCase()));
          if (activeRequests.length > 0) {
            reject(new Error('Can\'t create a new request because push request ' +
              activeRequests[0].id +
              ' hasn\'t completed yet. Try again later.'));
          } else {
            resolve(pushRequestsList);
          }
          reject(new Error('Push request filter error.'));
        });
      }
    
      /**
       * Monitor the push request until it finishes.
       *
       * Checks the status until the status is no longer an active status  (INPROGRESS or TRAINING)
       * or the number of attempts to check the status exceeds CONFIG.MAX_STATUS_RETRIES.
       *
       * @param {string} botid
       * @param {string} entityId
       * @param {string} pushRequestId
       * @returns {Promise<string>} Status
       */
    
      async _trackStatus(botId, entityId, pushRequestId) {
        try {
          logger.info(`Bot ID: ${botId}`);
          logger.info(`Entity ID: ${entityId}`);
          logger.info(`Push Requst ID: ${pushRequestId}`);
          logger.info('The push request job ${pushRequestId} is in progress. Waiting for the job to finish.');
          var pushRequestStatus = 'STATUS NOT YET RETRIEVED';
          let attemptsCount = 0;
          const exponentialBackOffFactor = 3;
          while (true) {
            const pushRequestData = await this._getPushRequest(botId, entityId, pushRequestId);
            pushRequestStatus = pushRequestData.result.status.toUpperCase();
            if (CONFIG.ACTIVE_STATUSES.includes(pushRequestStatus)) {
              // Not done yet
              // Check if maximum number of retries reached
              if (++attemptsCount > CONFIG.MAX_STATUS_RETRIES) {
                break;
              }          
              // Wait before retrying by implementing exponential back-off strategy using a factor of 3			
              logger.warn('The push request job is still in progress.');
              await this._sleep(attemptsCount * exponentialBackOffFactor * 1000); // ex. ( 1 (attemptCount) * 3 exponentialBackOffFactor ) seconds * 1000 milliseconds	
            } else {
              break;
            }
          }
          return pushRequestStatus;
        } catch (error) {
          const errorMessage = `Error tracking push request status. Detailed error: ${error.message} Request ID: ${error.opcRequestId}`;
          logger.error(errorMessage);
          throw new Error(errorMessage);
        }
      }
    
      /**
       * Wait for a specified time.
       * 
       * @param {number} duration - Wait duration in milliseconds
       * @returns 
       */
      sleep(duration) {
        return new Promise(resolve => setTimeout(resolve, duration));
      }
    
      /**
      * Get list of skills
      *
      * @returns {object} Results for GET skills
      */
      async _listSkills() {
        try {
          const URI = `https://${CONFIG.ODA_HOSTNAME}${CONFIG.DYNAMIC_ENTITIES_API_BASE_PATH}/skills`;
          return await OCIManager.send(URI, 'GET');
        } catch (error) {
          const errorMessage = `Error getting list of skills. Detailed error: ${error.message} Request ID: ${error.opcRequestId}`;
          logger.error(errorMessage);
          throw new Error(errorMessage);
        }
      }
    
      /**
      * Given a skills list, find the matching skill name and version
      * and then return the bot ID.
      *
      * @param {object} skillsData - Response from GET /api/v1/skills
      * @param {string} skillName - User-supplied name of skill
      * @param {string} skillVersion - User-supplied version of the skill
      * @returns {Promise <string>} Bot ID
      */
      async _getBotIdForSkillNameVersion(skillsData, skillName, skillVersion) {
        try {
          const matchingSkills = skillsData.result.filter(val => val.name === skillName  && val.version === skillVersion);
          if (matchingSkills.length === 0) {
            throw new Error(`Cannot find the skill with the name ${skillName} and version ${skillVersion}.`);
          } else {
            return(matchingSkills[0].id);
          }    
        } catch (error) {
          const errorMessage = `Error getting the bot ID for ${skillName} ${skillVersion}. Detailed error: ${error.message} Request ID: ${error.opcRequestId}`;
          logger.error(errorMessage);
          throw new Error(errorMessage);
        }
      }
    
      /**
       * Get the skills list and search it to get the bot ID for 
       * the matching skill name and version.
       *
       * @param {string} skillName - User-supplied name of skill
       * @param {string} skillVersion - User-supplied version of the skill
       * @returns {Promise<string>} Bot ID
       */
      async _lookupBotId(skillName, skillVersion) {
        try {
          return await this._listSkills().then(skillsData => this._getBotIdForSkillNameVersion(skillsData, skillName, skillVersion));
        } catch (error) {
          const errorMessage = `Error getting the bot ID for ${skillName} ${skillVersion}. Detailed error: ${error.message} Request ID: ${error.opcRequestId}`;
          logger.error(errorMessage);
          throw new Error(errorMessage);
        }    
      }
    
    
      /**
       * Get the list of the skill's entities.
       *
       * @param {string} botId
       * @returns {object} Results from GET bots/{botId}/dynamicEntities
       */
      async _getEntitiesForSkill(botId) {
        try {
          const URI = `https://${CONFIG.ODA_HOSTNAME}${CONFIG.DYNAMIC_ENTITIES_API_BASE_PATH}/bots/${encodeURIComponent(botId)}/dynamicEntities`;
          return await OCIManager.send(URI, 'GET');
        } catch (error) {
          const errorMessage = `Error getting the list of the skill's entities. Detailed error: ${error.message} Request ID: ${error.opcRequestId}`;
          logger.error(errorMessage);
          throw new Error(errorMessage);
        }
      }
    
      /**
       * Get the entity ID for the specified entity name from the entities data.
       *
       * @param {object} entitiesData - Response from GET /api/v1/bots/{botId}/dynamicEntities
       * @param {string} entityName - User-entered entity name
       * @returns {Promise<string>} Entity ID
       */
      async _getEntityId(entitiesData, entityName) {
        return new Promise((resolve, reject) => {
          const matchingEntities = entitiesData.result.filter(val => val.name === entityName);
          if (matchingEntities.length === 0) {
            reject(new Error(`Cannot find an entity with the name ${entityName}.`));
          } else {
            resolve(matchingEntities[0].id);
          }
          reject(new Error('Entity filter error.'));
        });
      }
    
      /**
       * Get the entity ID for the specified entity name and bot ID.
       *
       * @param {string} botId
       * @param {string} entityName User-entered entity name
       * @returns {Promise<string>} Entity ID
       */
      async _lookupEntityId(botId, entityName) {
        try {
          return await this._getEntitiesForSkill(botId).then(EntitiesData => this._getEntityId(EntitiesData, entityName));
        }
        catch (error) {
          const errorMessage = `Error looking up the entity ID for ${entityName} Request ID: ${error.opcRequestId}`;
          logger.error(errorMessage);
          throw new Error(errorMessage);
        }
      }
    
      /**
       * Create push request. This creates a container for
       * add, delete, and modify instructions for the
       * dynamic entity (patch data).
       *
       * @param {string} botId
       * @param {string} entityId
       * @param {boolean} copyQueryParm - Set to true to retain the original set, false to replace it
       * @returns {object} Response from POST bots/{botId}/dynamicEntities/{entityId}/pushRequest?copy={copyQueryParm}
       */
      async _createPushRequest(botId, entityId, copyQueryParm) {
        try {
          const body = '{}';
          const URI = `https://${CONFIG.ODA_HOSTNAME}${CONFIG.DYNAMIC_ENTITIES_API_BASE_PATH}/bots/${encodeURIComponent(botId)}/dynamicEntities/${encodeURIComponent(entityId)}/pushRequests?copy=${copyQueryParm}`;
          // Send request to start the job to push patch data to the dynamic entity
          return await OCIManager.send(URI, 'POST', body);
        } catch (error) {
          const errorMessage = `Error starting the push request job. Detailed error: ${error.message} Request ID ${error.opcRequestId}`;
          logger.error(errorMessage);
          throw error;
        }
      }
    
      /**
        * Push data to the push request. Uploads add, delete, and modify
        * instructions to the push request's container. Gets the instructions
        * from the specified JSON file
        * 
        * @param {string} botId
        * @param {string} entityId
        * @param {string} pushRequestId - The job ID that was returned when the patch request was created
        * @param {string} patchDataPath - The full path to the JSON file that contains the patch data
        * @returns {object} Response from PATCH bots/{botId}/dynamicEntities/{entityId}/pushRequests/{pushRequestId}/values
        *
        */
      async _pushDataToRequest(botId, entityId, pushRequestId, patchDataPath) {
        try {
          const body = require(patchDataPath);
          const URI = `https://${CONFIG.ODA_HOSTNAME}${CONFIG.DYNAMIC_ENTITIES_API_BASE_PATH}/bots/${encodeURIComponent(botId)}/dynamicEntities/${encodeURIComponent(entityId)}/pushRequests/${encodeURIComponent(pushRequestId)}/values`;
          return await OCIManager.send(URI, 'PATCH', body);
        } catch (error) {
          const errorMessage = `Error sending patch data to push request job ${pushRequestId}. Detailed error: ${error.message} Request ID ${error.opcRequestId}`;
          logger.error(errorMessage);
          throw error;
        }
      }
    
      /**
       * Mark the push request as DONE so that it can start processing patch data,
       * train the entity, and then save the changes. There isn't any response.
       * You can optionally use the method to abort the push request.
       *
       * @param {string} botId
       * @param {string} entityId
       * @param {string} pushRequestId
       * @param {boolean} abort optional parameter that when set to true
       * aborts the push request instead of finalizing it. Default false.
       * @returns
       */
      async _finalizePushRequest(botId, entityId, pushRequestId, abort) {
        try {
          let action = (abort) ? 'ABORT' : 'DONE';
          // todo in case the above doesn't work   
          //let action = 'DONE';
          //if (abort) {
          //	action = 'ABORT';
          //}
          const body = '{}';
          const URI = `https://${CONFIG.ODA_HOSTNAME}${CONFIG.DYNAMIC_ENTITIES_API_BASE_PATH}/bots/${encodeURIComponent(botId)}/dynamicEntities/${encodeURIComponent(entityId)}/pushRequests/${encodeURIComponent(pushRequestId)}/${encodeURIComponent(action)}`;
          return await OCIManager.send(URI, 'PUT', body);
        } catch (error) {
          const errorMessage = `Error finalizing push request. Detailed error: ${error.message} Request ID ${error.opcRequestId}`;
          logger.error(errorMessage);
          throw error;
        }
      }
    
      /**
       * Get push requests
       * 
       * Returns response from PATCH /api/v1/bots/{botId}/dynamicEntities/{entityId}/pushRequests
       *
       * @param botId
       * @param entityId
       * @returns Response from GET /api/v1/bots/{botId}/dynamicEntities/{entityId}/pushRequests
       */
      async _getPushRequests(botId, entityId) {
        try {
          const URI = `https://${CONFIG.ODA_HOSTNAME}${CONFIG.DYNAMIC_ENTITIES_API_BASE_PATH}/bots/${encodeURIComponent(botId)}/dynamicEntities/${encodeURIComponent(entityId)}/pushRequests`;
          return await OCIManager.send(URI, 'GET');
        } catch (error) {
          const errorMessage = `Error getting push requests. Detailed error: ${error.message} Request ID ${error.opcRequestId}`;
          logger.error(errorMessage);
          throw error;
        } 
      }
    
      /**
       * Get push request
       *
       * @param {string} botId
       * @param {string} entityId
       * @param {string} pushRequestId The job ID
       * @returns Response from GET /api/v1/bots/{botId}/dynamicEntities/{entityId}/pushRequests/{id}
       */
      async _getPushRequest(botId, entityId, pushRequestId) {
        try {
          const URI = `https://${CONFIG.ODA_HOSTNAME}${CONFIG.DYNAMIC_ENTITIES_API_BASE_PATH}/bots/${encodeURIComponent(botId)}/dynamicEntities/${encodeURIComponent(entityId)}/pushRequests/${encodeURIComponent(pushRequestId)}`;
          return await OCIManager.send(URI, 'GET');
        } catch (error) {
          const errorMessage = `Error getting push request. Detailed error: ${error.message} Request ID ${error.opcRequestId}`;
          logger.error(errorMessage);
          throw error;
        }
      }
    }
    
    module.exports = new ODAManager();
  9. From a command terminal, change to the top level directory and run this command to install the libraries:

    npm install

Prepare the Dynamic Entity Data

Create a JSON file that contains the request body (the values to add, modify, and delete). Here's the schema:

{
    "type":"object",
    "properties":{
        "delete":{
            "type":"array",
            "description":"The entity values to delete. The service ignores this object if the copy query parameter for the push request was omitted or set to FALSE.",
            "items":{
                "type":"object",
                "properties":{
                     "canonicalName":{
                          "type":"string",
                          "description":"The entity value."
                      },
                      "synonyms":{
                          "type":"array",
                          "description":"Synonyms for the entity value.",
                          "items":{
                              "type":"string",
                              "description":"Synonyms for the entity value."
                          }
                      },
                      "primaryLanguageCanonicalName":{
                          "type":"string",
                          "description":"The entity value (canonicalName) for the primary language. This property is required for secondary-language entity values for native multi-language skills. If you include it for a primary-language entity value, then it must be equal to the canonicalName value. The property must not be included for skills that aren't native multi-language."
                      },
                      "nativeLanguageTag":{
                          "type":"string",
                          "description":"The native language tag to use for the entity value. The tag must identify one of the languages that the skill supports. This property is required for native multi-language skills, even for entries for the primary language. This property is not valid for skills that aren't native multi-language."
                      }
                  }
            }
        },
        "add":{
            "type":"array",
            "description":"The entity values to add.",
            "items":{
                see delete
            }
        },
        "modify":{
            "type":"array",
            "description":"The entity values to modify. The service ignores this object if the <code>copy</code> query parameter for the push request was omitted or set to <code>FALSE</code>.",
            "items":{
                see delete
            }
        }
    },
    "description":"The data to add, delete, and modify."
}

Here's an example for a native multi-language skill. Note that the native language tag is the first 2 letters of the natively-supported language's ISO 639-1 code.

{
  "add": [{
      "canonicalName": "department",
      "synonyms": ["division"],
      "nativeLanguageTag": "en"
    }, {
      "canonicalName": "abteilung",
      "synonyms": ["bereich"],
      "primaryLanguageCanonicalName": "department",
      "nativeLanguageTag": "de"
    }, {
      "canonicalName": "departement",
      "primaryLanguageCanonicalName": "department",
      "nativeLanguageTag": "fr"
    }
  ]
}

Here's an example for a skill that isn't native multi-language:

{
  "add": [{
      "canonicalName": "ABZ",
      "synonyms": ["Aberdeen", "Aberdeen Dyce"]
    }, {
      "canonicalName": "AAL",
      "synonyms": ["Aalborg"]
    }, {
      "canonicalName": "JAN",
      "synonyms": ["Jackson-Medgar Wiley Evers International", "Jackson International"]
    }
  ]
}

Run the Script

  1. Open the config/config.js file and set the desired run configurations including the path to the JSON file that contains the request body, as described in the previous section.

    /*
     * Run configurations 
     */
    
    // Full path of the JSON file that contains the patch data
    module.exports.DYNAMIC_ENTITIES_PATCH_DATA_PATH = '~/patchData.json';
    
    // Name of the skill to upload the dynamic entities to
    module.exports.ODA_SKILL_NAME = '';
    // Version of the named skill
    module.exports.ODA_SKILL_VERSION = '';
    // Name of the dynamic entity to modify
    module.exports.ODA_ENTITY_NAME = '';
    // Set copy to true to retain the original value set
    module.exports.DYNAMIC_ENTITIES_COPY = true;
  2. From a terminal, change to the top-level directory, and then enter this command to run the script:

    npm start

    You should see output similar to this:

    [2021-09-14T12:14:33.489] [INFO] ODA Manager - Entity ID: 25CB0ED2-2D9B-44B5-911F-7A95AD31EFD1
    [2021-09-14T12:14:33.490] [INFO] ODA Manager - Push Requst ID: DADECC1A-8744-4F49-9FD6-DF0F82F6AD83
    [2021-09-14T12:14:33.490] [INFO] ODA Manager - The push request job DADECC1A-8744-4F49-9FD6-DF0F82F6AD83 is in progress. Waiting for the job to finish.
    [2021-09-14T12:14:34.075] [INFO] ODA Manager - The push request job has finished. Status = COMPLETED.
    [2021-09-14T12:14:34.076] [INFO] ODA Manager - Total deleted: 0
    [2021-09-14T12:14:34.076] [INFO] ODA Manager - Total added: 3
    [2021-09-14T12:14:34.077] [INFO] ODA Manager - Total modified: 0
    [2021-09-14T12:14:34.077] [INFO] Dynamic Entities Importer - Dynamic entity updated.