Four Re-usable Custom Components for Oracle Digital Assistant

December 18, 2020 | 4 minute read
Text Size 100%:

Introduction

In a few recent POCs involving Oracle Digital Assistant (ODA) and SaaS, we have seen a number of use cases from various customers that required retrieving information via SOAP services and displaying it in ODA conversations. One example of such SOAP service is BIP Report SOAP API. Services exposed by Custom Components (CC) in ODA are implemented in Javascript, which works naturally with JSON style objects. SOAP services use XML. So it is necessary to convert XML to JSON and vise versa when SOAP services are used in ODA CC. The BIP Report SOAP Service, in particular, also requires BASE64 decoding as its response payload is BASE64 encoded.

In this blog, I would like to show that we can easily implement these basic functionalities in re-usable CC's. In a follow-up post, I will demonstrate how we can leverage these basic CC's to implement a higher level CC that works with BIP Report SOAP services. The goal of this and the follow-up post is to promote reusability in the ODA Component Service layer.

What's Covered in this Post?

In addition to the three custom components mentioned below, this post also includes a mock-up test client for testing the CC's as stand-alone Node.js programs.

List of Reusable Custom Components

  • BASE64: Performs BASE64 encoding and decoding
  • XML2JSON: Converts an XML string to a JSON object
  • JSON2XML: Converts a JSON object to an XML string

Create A Basic Structure for Custom Components

As this post is not intended to be a tutorial for creating an ODA CC, I will refer the readers to the tutorial Create Custom Components to Integrate with Backend Services for the necessary initial steps for building a CC. After that, you should have a directory structure similar to the following image.

CC Directory Structure

With the directory structure ready, we can now add custom component files to the components sub-directory.

BASE64: Base64.js

'use strict';
 
module.exports = {
  metadata: () => ({
    name: 'Base64',
    properties: {
      inputString: { required: true, type: 'string' },
      encode: { required: true, type: 'boolean'},
      outputVariableName: {required: true, type: 'string'},
      errorMessageVariableName: {required: false, type: 'string'},
      keepTurn: {required: false, type: 'boolean'}
    },

    supportedActions: ['success', 'failure']
  }),
  invoke: (conversation, done) => {
    var logger = conversation.logger();

    // perform conversation tasks.
    const { inputString } = conversation.properties();
    const { encode } = conversation.properties();
    const { outputVariableName } = conversation.properties();
    const { errorMessageVariableName } = conversation.properties();
    const { keepTurn } = conversation.properties().keepTurn ? conversation.properties() : {keepTurn: false};

    try {
      var result;
      if (encode) {
        result = module.exports.base64Encode(inputString);
      }
      else {
        result = module.exports.base64Decode(inputString);
      }
      conversation.variable(outputVariableName, result);
      conversation.transition('success');
    }
    catch(err) {
      logger.error("Base64 error: " + err.message);
      conversation.variable(errorMessageVariableName, err.message);
      conversation.transition('failure');
    }
    conversation.keepTurn(keepTurn);
    done();
  },

  base64Encode: function(stringVar) {
    var buff = new Buffer.from(stringVar, 'ascii');
    return buff.toString('base64');
  },

  base64Decode: function(stringVar) {
      var buff = new Buffer.from(stringVar, 'base64');
      return buff.toString('ascii');
  }

};
 

XML2JSON: XmlToJson.js

'use strict';
 
var xml2js = require('xml2js');

module.exports = {
  metadata: () => ({
    name: 'XmlToJson',
    properties: {
      xmlString: { required: true, type: 'string' },
      parserOptions: { required: false, type: 'string'},
      responseVariableName: {required: false, type: 'string'},
      errorMessageVariableName: {required: false, type: 'string'},
      keepTurn: {required: false, type: 'boolean'}
    },

    supportedActions: ['success', 'failure']
  }),
  invoke: (conversation, done) => {
    var logger = conversation.logger();

    logger.debug('In XmlToJson');

    // perform conversation tasks.
    const { xmlString } = conversation.properties();
    const { responseVariableName } = conversation.properties();
    const { parserOptions } = conversation.properties();
    const { errorMessageVariableName } = conversation.properties();
    const { keepTurn } = conversation.properties().keepTurn ? conversation.properties() : {keepTurn: false};
    
    try {
      var parser = new xml2js.Parser();
      if (typeof parserOptions !== 'undefined') {
        logger.debug('parser options = ' + parserOptions);
        parser = new xml2js.Parser (JSON.parse(parserOptions));
      }
  
      //logger.debug('Input xml string: \n' + xmlString);
  
      parser.parseString(xmlString, function (err, result) {
        logger.debug('XML to JSon done');

        if (err) {
            logger.error("XmlToJson error: " + err.message);
            conversation.variable(errorMessageVariableName, err.message);
            conversation.transition('failure');
            conversation.keepTurn(keepTurn);
            done();
            return;
          }

          conversation.variable(responseVariableName, result);
          conversation.transition('success');
          conversation.keepTurn(keepTurn);
          done();
      });
    }
    catch(err) {
      logger.error("JsonToXml error: " + err.message);
      conversation.variable(errorMessageVariableName, err.message);
      conversation.transition('failure');
      conversation.keepTurn(keepTurn);
      done();
    }
  }
};
 

JSON2XML: Json2Xml.js

'use strict';
 
var xml2js = require('xml2js');

module.exports = {
  metadata: () => ({
    name: 'JsonToXml',
    properties: {
      jsonString: { required: true, type: 'string' },
      builderOptions: { required: false, type: 'string'},
      responseVariableName: {required: false, type: 'string'},
      errorMessageVariableName: {required: false, type: 'string'},
      keepTurn: {required: false, type: 'boolean'}
    },

    supportedActions: ['success', 'failure']
  }),
  invoke: (conversation, done) => {
    var logger = conversation.logger();

    logger.debug('In JsonToXml');

    // perform conversation tasks.
    const { jsonString } = conversation.properties();
    const { responseVariableName } = conversation.properties();
    const { builderOptions } = conversation.properties();
    const { errorMessageVariableName } = conversation.properties();
    const { keepTurn } = conversation.properties().keepTurn ? conversation.properties() : {keepTurn: false};

    try {
      var bOptions = builderOptions;
      if (! builderOptions) {
        bOptions = {};
      }
      else if (typeof builderOptions === 'string' || builderOptions instanceof String) {
        bOptions = builderOptions;
      }
      else {
        bOptions = JSON.stringify(builderOptions);
      }

      var xmlString = module.exports.jsonToXml(jsonString, bOptions);
      conversation.variable(responseVariableName, xmlString);
      conversation.transition('success');
    }
    catch(err) {
      logger.error("JsonToXml error: " + err.message);
      conversation.variable(errorMessageVariableName, err.message);
      conversation.transition('failure');
    }
    conversation.keepTurn(keepTurn);
    done();
  },
  jsonToXml: function(jsonString, builderOptions) {
    var builder = new xml2js.Builder();
    if (builderOptions) {
      builder = new xml2js.Builder (builderOptions);
    }

    var xmlString = builder.buildObject(JSON.parse(jsonString));
    return xmlString;
  }
};
 

A Simple Mock-up Test Client: testXmlToJson.js

It is not well documented but there is a simple mock-up test client included in the Oracle Bots Node SDK package. It allows developers to test custom components as stand-alone programs. The code below shows a sample of how you can use the test client to run the XmlToJson custom component.

'use strict';

const testing = require('@oracle/bots-node-sdk/testing');
const xmlToJson = require('./components/XmlToJson.js');

const request = testing.MockRequest(
                  {}, 
                  {
                   'xmlString': '<root><array><item>1</item><item>2</item></array>greetings>welcome</greetings>/root>',
                   'parserOptions': '{"explicitArray":false}',
                   'responseVariableName': 'responseJson',
                   'errorMessageVariableName': 'errorMsg',
                   'keepTurn': true
                  }
                );
            
const conversation = testing.MockConversation.fromRequest(request);

conversation.logger().debug = console.log;
conversation.logger().info = console.log;
conversation.logger().warn = console.log;
conversation.logger().error = console.log;

xmlToJson.invoke(conversation, err => {
        console.log(JSON.stringify(conversation.variable('responseJson')));
});
 

Run the Test Client

If the test Python file is located inside the package directory, you can run the test client in a terminal with the following command:

node testXmlToJson.js

If the test client is located in another directory, the relative path to the custom component file may need to be modified.

Conclusion

In this post, I presented three simple ODA custom components that can be deployed to ODA as services. But more importantly, they serve as reusable building blocks for creating more ODA custom components that implement business functions as ODA services. I intend to complete the reusability demonstration in a follow-up post. 

 

 

Siming Mu


Previous Post

When Jupyter Aligns ...

John Featherly | 4 min read

Next Post


Preparing Network Gateways for Private Oracle Analytics Cloud Data Sources

Dayne Carley | 7 min read