24 Backend Integration

You can integrate skills with backend services, such as Oracle Human Capital Management Cloud and Oracle Financials Cloud, through custom components. For example, you might want to create a skill that lets your employees report their travel expenses to Oracle Financials Cloud.

To integrate a skill with a backend service:

  1. Implement: Using JavaScript and the Oracle Digital Assistant Node.js SDK, implement a custom component that transfers data to and from the skill using the SDK's metadata and conversation objects. See Task 1: Implement Custom Components.

  2. Deploy: If you are hosting the component components on Oracle Mobile Hub backend, or a Node.js server, deploy the component package. See Task 2: Deploy the Component Package to a Service.

  3. Add to Skill: Make the components available to a skill by adding a component service for it. See Task 3: Add Component Package to a Skill.

Task 1: Implement Custom Components

To implement custom components, you use the Oracle Digital Assistant Node.js SDK to interface with Digital Assistant's custom component service.

Here's how to implement custom components that you can deploy to the Digital Assistant embedded container, a Mobile Hub backend, or a Node.js server:

  1. Install the software for building custom components.

  2. Create the custom component package.

  3. Create and build a custom component.

Note:

If you plan to deploy the custom component package to an embedded custom component service, each skill that you add the package to is counted as a separate service. There's a limit to how many embedded custom component services an instance can have. If you don't know the limit, ask your service administrator to get the embedded-custom-component-service-count for you as described in View Service Limits in the Infrastructure Console. Consider packaging several components per package to minimize the number of embedded component services that you use. If you try to add a component service after you meet that limit, the service creation fails.

You can find up-to-date examples, as well as more detailed instructions at https://github.com/oracle/bots-node-sdk/tree/master/examples/custom-components/starter.

Step 1: Install the Software for Building Custom Components

To build a custom component package, you need Node.js, Node Package Manager, and the Oracle Digital Assistant Bots Node.js SDK.

Note:

If you plan to deploy to the embedded container, your package should be compatible with the following versions:
  • For Oracle Digital Assistant instances that are provisioned on Oracle Cloud Infrastructure (sometimes referred to as the Generation 2 cloud infrastructure), your package should be compatible with Node.js 8.11.5 and Bots Node SDK 2.2.2.
  • For Oracle Digital Assistant instances that are provisioned on Oracle Cloud Platform (as all version 19.4.1 instances are), your package should be compatible with 8.11.4 and Bots Node SDK 2.2.0.
  1. If you haven’t already, download Node.js from https://nodejs.org and install it for global access. Node Package Manager (npm) is distributed with Node.js.

    To test if Node.js and npm are installed, open a terminal window and type these commands:

    node –v 
    npm –v
  2. To install the Oracle Digital Assistant Bots Node.js SDK for global access, enter this command in a terminal window:
    npm install -g @oracle/bots-node-sdk

    On a Mac, you use the sudo command:

    sudo npm install -g @oracle/bots-node-sdk

    When you use the -g (global) option, you have direct access to the bots-node-sdk command line interface. Otherwise, use npx @oracle/bots-node-sdk.

  3. To verify your Oracle Digital Assistant Bots Node.js SDK installation, type the following command:
    bots-node-sdk -v
    The command should print the Oracle Digital Assistant Bots Node.js SDK version.

Step 2: Create the Custom Component Package

Use the SDK’s command line interface (CLI) to create the necessary files and directory structure.

To create the package folder, and the necessary contents, type the following command in a terminal window:

bots-node-sdk init <top-level folder path>

This command completes the following actions:

  • Creates the top-level folder.

  • Creates a components folder and adds a sample component JavaScript file named hello.world.js. This is where you'll put your component JavaScript files.

  • Adds a package.json file, which specifies main.js as the main entry point and lists @oracle/bots-node-sdk as a devDependency. The package file also points to some bots-node-sdk scripts.

    {
      "name": "myCustomComponentService",
      "version": "1.0.0",
      "description": "Oracle Bots Custom Component Package",
      "main": "main.js",
      "scripts": {
        "bots-node-sdk": "bots-node-sdk",
        "help": "npm run bots-node-sdk -- --help",
        "prepack": "npm run bots-node-sdk -- pack --dry-run",
        "start": "npm run bots-node-sdk -- service ."
      },
      "repository": {},
      "dependencies": {},
      "devDependencies": {
        "@oracle/bots-node-sdk": "^2.2.2",
        "express": "^4.16.3"
      }
    }
  • Adds a main.js file, which exports the package settings and points to the components folder for the location of the components, to the top-level folder.

  • Adds an .npmignore file to the top-level folder. This file is used when you export the component package. It must exclude .tgz files from the package. For example: *.tgz.

  • For some versions of npm, creates a package-lock.json file.

  • Installs all package dependencies into the node_modules subfolder.

Note:

If you don't use the bots-node-sdk init command to create the package folder, then ensure that the top-level folder contains an .npmignore file that contains a *.tgz entry. For example:
*.tgz
spec
service-*

Otherwise, every time you pack the files into a TGZ file, you include the TGZ file that already exists in the top-level folder, and your TGZ file will continue to double in size.

If you plan to deploy to the embedded container, your package should be compatible with the following versions:

  • For Oracle Digital Assistant instances that are provisioned on Oracle Cloud Infrastructure (sometimes referred to as the Generation 2 cloud infrastructure), your package should be compatible with Node.js 8.11.5 and Bots Node SDK 2.2.2.
  • For Oracle Digital Assistant instances that are provisioned on Oracle Cloud Platform (as all version 19.4.1 instances are), your package should be compatible with 8.11.4 and Bots Node SDK 2.2.0.

This step shows the basic CLI command. For more information, see https://github.com/oracle/bots-node-sdk/blob/master/bin/CLI.md.

Step 3: Create and Build a Custom Component

Here are the steps for building each custom component in your package:

  1. Create the component JavaScript file.

  2. Add code to the metadata and invoke functions.

  3. Access the backend.

  4. Use the SDK to access request and response payloads.

  5. Ensure that the component works in digital assistants.

Create the Component JavaScript File

Use the SDK's CLI to create a JavaScript file with the required functions for the component.

From a terminal window, CD to the package’s top-level folder and type the following command:

bots-node-sdk init component –-name <component name> components

The <component name>.js file, which contains the framework for working with the Oracle Digital Assistant Node.js SDK, is added to the components folder.

Note that the component name can't exceed 100 characters. You can only use alphanumeric characters and underscores in the name. You can't use hyphens. Nor can the name have a System. prefix. Oracle Digital Assistant won't allow you to add a custom component service that has invalid component names.

This step shows the basic CLI command. For more information, see https://github.com/oracle/bots-node-sdk/blob/master/bin/CLI.md.

Add Code to the metadata and invoke Functions

The required interface for a custom component implementation is:

// interface for a custom component implementation
{
  metadata(): {name: string, properties?: {[name:string]: string}, supportedActions?: string[]};
  invoke(conversation: Conversation, done: () => {}): void;
}
  1. Update the metadata function.

    This function provides the component descriptions that are displayed in the Components tab in the UI, and describes how to use the component in the dialog flow. Modify the metadata to provide the component name (which must be unique), the names and types of the input parameters that it expects, and the actions that the component can return, if any.

  2. Add code to the invoke function to make REST calls, access request data, update response data, and return to the skill.

    The invoke function takes two arguments:

    • conversation, which is an instance of the Conversation class in the Digital Assistant Node.js SDK. This class is described in in the SDK documentation at https://oracle.github.io/bots-node-sdk/.
    • done, which is a callback that the component invokes when it has finished processing

Here’s an example:

'use strict';

module.exports = {
    metadata: () => ({
        name: 'hello.world',
        properties: {
            human: {
                required: true,
                type: 'string'
            },
        },
        supportedActions: []
    }),
    invoke: (conversation, done) => {
        // Perform conversation tasks.
        const { human } = conversation.properties();
        conversation
            .reply(`Greetings ${human}.`)
            .reply(`Today is ${new Date().toDateString()}.`)
            .transition();

        done();
    }
};
Control the Flow with keepTurn and transition

You use different combinations of the Bots Node SDK keepTurn and transition functions, as well as the call to done(), to define how the custom component interacts with a user and how the conversation continues after the component returns flow control to the skill.

  • keepTurn(boolean) specifies whether the conversation should transition to another state without first prompting for user input.

    Note that if you want to set keepTurn to true, you should call keepTurn after you call reply because reply implicitly sets keepTurn to false.

  • transition(action) causes the dialog to transition to the next state after all replies, if any, are sent. The optional action argument names that action (outcome) that the component returns.

    If you don't call transition(), then, when you invoke done(), the response is sent but the dialog stays in the state and subsequent user input comes back to this component. That is, invoke() is called again.

  • done() signals that the custom component request has been processed and that the server response should be sent. You must always call it once per invoke().

invoke: (conversation, done) ==> {
   ...
   conversation.reply(payload);
   conversation.keepTurn(true);
   conversation.transition ("success"); 
   done();
}

Here are some common use cases where you would use keepTurn and transition to control the dialog flow:

Use Case Values Set for keepTurn and transition

A custom component that transitions to another state without first prompting the user for input.

  1. If applicable, use conversation.reply(<reply>) to send a reply.

  2. Set conversation.keepTurn(true).

  3. Set conversation.transition with either a supportedActions string (e.g., conversation.transition("success")) or with no argument (e.g., conversation.transition()).

  4. Call done() to signal that the custom component request has been processed and the server response should be sent.

For example, this custom component updates a context variable with a list of values to be immediately displayed by a System.List component, which the skill has defined for the next state in the dialog flow definition.
invoke: (conversation, done) => {
    const listVariableName = conversation.properties().variableName;
    ...
    // Write list of options to a context variable
    conversation.variable(listVariableName, list);
   // Navigate to next state without 
   // first prompting for user interaction.
   conversation.keepTurn(true);
   conversation.transition();
   done();
 }

A custom component that enables the skill to wait for input after control returns to the skill and before the skill transitions to another state.

  1. If applicable, use conversation.reply(<reply>) to send a reply.

  2. Set conversation.keepTurn(false) .

  3. Set conversation.transition with either a supportedActions string(conversation.transition("success")) or with no arguments (conversation.transition()).

  4. Call done() to signal that the custom component request has been processed and the server response should be sent.

For example:
conversation.keepTurn(false);
conversation.transition("success");
done();
A custom component that gets user input without returning flow control back to the skill. For example:
  • A component passes the user input to query a backend search engine. If the skill can only accommodate a single result, but the query instead returns multiple hits, the component prompts the user for more input to filter the results. In this case, the custom component continues to handle the user input; it holds the conversation until the search engine returns a single hit. When it gets a single result, the component calls conversation.transition() to move on to another state as defined in the dialog flow definition.

  • A component processes a questionnaire and only transitions to another next state when all questions are answered.

  1. Do not call transition.

  2. Set keepTurn(false).

  3. Call done() to repeat the invocation.

For example, this custom component outputs a quote and then displays Yes and No buttons to request another quote. It transitions back to the skill when the user clicks No.
  invoke: (conversation, done) => {
    // Perform conversation tasks.
    const tracking_token = "a2VlcHR1cm4gZXhhbXBsZQ==";    
    var quotes = require("./src/Quotes.json");
    var quote = quotes[Math.floor(Math.random() * quotes.length)];
    
    // Check if postback action is issued. If postback action is issued, 
    // check if postback is from this component rendering. This ensures
    // that the component only responds to its own postback actions.     
    if (conversation.postback() && conversation.postback().token == tracking_token && conversation.postback().isNo) {
      conversation.keepTurn(true);
      conversation.transition();
      done();
    } else {
      // Show the quote of the day.
      conversation.reply("'" + quote.quote + "'");
      conversation.reply(" Quote by: " + quote.origin);
      // Create a single message with two buttons to 
      // request another quote or not.
      let actions = [];
      actions.push(conversation.MessageModel().postbackActionObject("Yes", null, { isNo: false, token: tracking_token }));
      actions.push(conversation.MessageModel().postbackActionObject("No", null, { isNo: true, token: tracking_token }));
      let buttonMessage = conversation.MessageModel().textConversationMessage("Do you want another quote?", actions, null);
      conversation.reply(buttonMessage);
      // Although reply() automatically sets keepTurn to false, 
      // it's good practice to explicitly set it so that it's
      // easier to see how you intend the component to behave.
      conversation.keepTurn(false);
      done();
    };
  }

If a component doesn’t transition to another state before it calls done(), then it needs to keep track of its own state, as shown in the above example.

For more complex state handling, such as giving the user the option to cancel if a data retrieval is taking too long, you can create and use a context variable. For example: conversation.variable("InternalComponentWaitTime", time). If you use a context variable, don't forget to reset it or set it to null before calling conversation.transition.

Note that as long as you don't transition, all values that are passed in as component properties are available, even after you call done().

The component invocation repeats without user input. For example:

  • A component pings a remote service for the status of an order until the status is returned as accepted or the component times out. If the accepted status is not returned after the fifth ping, then the component transitions with the failedOrder status.

  • The custom component hands the user over to a live agent. In this case, the user input and responses get dispatched to the agent. The component transitions to another state when either the user or the agent terminates their session.

  • Do not call transition.

  • Set conversation.keepTurn(true).

  • Call done() to repeat the invocation.

Here's a somewhat contrived example that shows how to repeat the invocation without waiting for user input, and then how to transition when done:
  invoke: (conversation, done) => {
    // Perform conversation tasks.   
    var quotes = require("./src/Quotes.json");
    var quote = quotes[Math.floor(Math.random() * quotes.length)];
    // Show 5 quotes of the day
    conversation.variable("InternalQuoteCount", 
        conversation.variable("InternalQuoteCount") + 1 || 1);
    if (conversation.variable("InternalQuoteCount") < 6) {
      // Show the quote of the day.
      conversation.reply("'" + quote.quote + "'");
      conversation.reply(" Quote by: " + quote.origin);
      // Don't prompt for user input
      conversation.keepTurn(true);
      // Don't transition. Start over to show another quote.
      done();
    } else {
      // Reset quote count
      conversation.variable("InternalQuoteCount", 0);
      // Transition to another state without prompting for user input
      conversation.keepTurn(true);
      conversation.transition();
      done();
    };
  }
Access the Backend

Node.js provides the HTTPS module for sending HTTPS requests to access backends. Its documentation is at https://nodejs.org/api/https.html. You'll find that there are several Node.js libraries that have been built to make HTTP requests easy, and the list changes frequently. You should review the pros and cons of the currently available libraries and decide which one works best for you.

Use the SDK to Access Request and Response Payloads

Use Conversation instance methods to get the context for the invocation, change variables, and send results back to the dialog engine.

Here's how you use these methods for some common tasks:

How Do I? Do this
Get a component property value. Call conversation.properties() to get an object that contains all the component properties.
const MyCustomComponent = {module.exports = {
   metadata: () => ({
     name: 'greeting',
     properties: {
       "name": {        
         "type": "string",
         "required": true
     }    
  }   
}),
invoke: (conversation, done) => {
  // Get property values
  const { name: _name = '' } = conversation.properties();
  conversation.reply('Hello ${_name}');
  conversation.transition();
  done();
  }
}
Get the value of a context variable. Add a metadata property to pass in the name of the variable, verify that the property was passed in, and then call conversation.variable(<variable name>)to retrieve the value.
const { latitudeVariable } = conversation.properties();
if (latitudeVariable) {
  let _latitude = conversation.variable(latitudeVariable); 
  // ...   
  conversation.transition();      
  done()
} else {
  done(new Error('State is missing latitudeVariable property.'));
}
Get the value of a profile variable. Call conversation.variable('profile.<name>') to retrieve the value.
Get the value of a skill's custom parameter. On the skill's Settings tab, add the custom parameter and provide a value (or set the value in the dialog flow). From the custom component, call conversation.variable(system.config.<parameter-name>') to retrieve the value.
Set a context variable's value. Add a metadata property to pass in the name of the variable, and then call conversation.variable(<variable name>, <value>) to set the value. For example:
if (latitudeVariable) {conversation.variable(latitudeVariable, _coordinates.x);}
Use an object to update a variable that's an entity type, such as ADDRESS, and include an entityName property that's set to the entity type.
if (addressVariable){
  let _AddressObject = {};
  _AddressObject['entityName'] = 'ADDRESS';
  _AddressObject['postCode'] = _zip;
  _AddressObject['state'] = _state; 
  conversation.variable(conversation.properties().addressVariable, _addressObject);
}

Note that if the context variable doesn't exist, it will be created. However, subsequent use in the dialog flow will be flagged for validation warning.

Set System.CommonResponse - text labels and values. Add a metadata property to pass in the name of the iterator variable, and then call conversation.variable(<variable name>, <value>) to create a JSON object with the label and value for each choice. For example:
    metadata: () => (
    {
        "name": "populate.list.variable",
        "properties": {
            "variableName": { "type": "string", "required": true }
        },
        ...
    }
    ...
    invoke: (conversation, done) => {
        const _variable = conversation.properties().variableName;
        // Add label and value for each choice to JSON object
        let banana = {label:'Banana', value:'banana'};
        let apple = {label:'Apple', value:'apple'};
        let orange = {label:'Orange', value:'orange'};
        let data = [];
        data.push(banana);
        data.push(apple);
        data.push(orange);
        conversation.variable(_variable, data);
        conversation.transition();
        done();
    }
Set the value of a Composite Bag item. In the skill, create a context variable and set its type to the name of the Composite Bag entity. In the custom component, add metadata properties to pass in the name of the Composite Bag entity variable and the name of the bag item. Add code to check if the entity variable is null, and set it to and empty object if it is. Create an object for the bag item with the desired values and set the bag item to that object. Update the entity variable and return to the skill.

Tip:

You can use the intent tester to see the attributes for a complex built-in type.

Here's an example:

metadata: () => ({
  name: 'sample.SetEmailBagItem',
  properties: {
    variableName: { required: true, type: 'string' },
    bagItem: { required: true, type: 'string' },
    email: { required: true, type: 'int' },
  },
  supportedActions: ["validEmail", "invalidEmail"]
}),
...
  // code to get property values and validate email goes here
  ...
  // Get composite bag entity object (cbe), create bag item object, update bag item in cbe 
  let cbe = conversation.variable(variableName);
  if (cbe == null) {
    cbe = {};
  }
  let _object = {};
  _object.email = email;
  _object.entityName = "EMAIL";
  cbe[bagItem] = _object;
  conversation.variable(variableName, cbe);
  conversation.transition("validEmail");
  conversation.keepTurn(true);
  done();
Send a reply or a message response. Call conversation.reply(<payload>). The payload can be a string, an object, or a MessageModel. You can call this function multiple times. When you call this function, keepTurn is set to false automatically.
Display a text response with postback action buttons. Create an array of postback action objects, call textConversationMessage() with the message and the action array, and pass the result in a conversation.reply().
let actions = [];
actions.push(
  conversation.MessageModel().postbackActionObject("Yes", null, { isNo: false }));
actions.push(
  conversation.MessageModel().postbackActionObject("No", null, { isNo: true }));
let buttonMessage = 
  conversation.MessageModel().textConversationMessage("Do you want another quote?", actions, null);
conversation.reply(buttonMessage);

Add code at the start of the invocation to detect and process the postback action.

if (conversation.postback() 
    && conversation.postback().isNo) {
  conversation.keepTurn(true);
  conversation.transition();
  done();
}
Render a card layout and return the selection as an action. Create an array of card objects, each with the desired postback action. Create a card conversation message from the card array and send it as a reply (without a transition). Add an if condition at the start of the invocation to detect the postback and transition with the chosen action. You'll also need to add the card's actions to the metadata.
if (conversation.postback()) {
  conversation.keepTurn(true);
  conversation.transition(conversation.postback().action);
  done();
} else {
  var actions = [];
  actions.push(
    conversation.MessageModel().postbackActionObject(
      'Oranges', null, { action: 'oranges' }));
  var messageModel = conversation.MessageModel();
  var cards = [];
  cards.push(messageModel.cardObject(
    '4 Dozen Oranges',
    '4 dozen Mandarin oranges in a wooden crate.',
    null, null, actions));
  actions = [];
  actions.push(
    conversation.MessageModel().postbackActionObject(
      'Grapes', null, { action: 'grapes' }));
  cards.push(messageModel.cardObject(
    'Carton of Grapes',
    '10kg ripe grapes in a protected carton.',
    null, null, actions));
  var cardsResponse = messageModel.cardConversationMessage(
    'vertical', cards);
  conversation.logger().info('Replying with card response');
  conversation.reply(cardsResponse);
  done();
}
Render an attachment. Use the message model's attachmentConversationMessage() method to create an attachment message. For example:
  var messageModel = conversation.MessageModel();  
  let attachmentMessage =
    messageModel.
      attachmentConversationMessage(
        attachmentType, attachmentUrl);
  conversation.reply(attachmentMessage);

The attachment type can be image, video, audio, or file.

You can display action URLs under the attachment, such as links to more videos and tutorials as shown here:
  const videosUrl="https://example.com/videos.html";
  const tutorialsUrl = "https://example.com/tutorials.html";
  var urlVideoAction =
    conversation.MessageModel().urlActionObject('More Videos', null, videosUrl);
  var urlTutorialAction =
    conversation.MessageModel().urlActionObject('More Tutorials', null, tutorialsUrl);
  var messageModel = conversation.MessageModel();  
  let attachmentMessage =
    messageModel.
      attachmentConversationMessage(
        attachmentType, attachmentUrl,
       [urlVideoAction, urlTutorialAction]);
  conversation.reply(attachmentMessage);
Return to the dialog flow. Call transition(<optional state>) followed by done() to return to the dialog flow and transition to another state. Use transition() to transition to the next state in the flow. Otherwise, use transition(<action>), where <action> is one of the supportedActions in the metadata.
module.exports = {
  metadata: () => ({
    name: 'verify',
    properties: {
      "address": {
        "type": "string",
        "required": false
      }
    },
    supportedActions: ['valid', 'invalid']
  }),
  invoke: (conversation, done) => {
    ...
    conversation.transition('valid');
    done();
    ...
}};

To learn more about transitions, see Control the Flow with keepTurn and transition.

Pass a JSON object to the custom component. In the custom component's metadata, specify a property of type string or map (either will work). In the dialog flow, use a FreeMarker expression like this to pass the JSON object:
    properties:
      object: "${myJSONObjectVarName.value}"

The full SDK documentation is at https://github.com/oracle/bots-node-sdk.

Ensure the Component Works in Digital Assistants

In a digital assistant conversation, a user can break a conversation flow by changing the subject. For example, if a user starts a flow to make a purchase, they might interrupt that flow to ask how much credit they have on a gift card. We call this a non sequitur. To enable the digital assistant to identify and handle non sequiturs, call the conversation.invalidInput(payload) method when a user utterance response is not understood in the context of the component.

In a digital conversation, the runtime determines if an invalid input is a non sequitur by searching for response matches in all skills. If it finds matches, it reroutes the flow. If not, it displays the message, if provided, prompts the user for input, and then executes the component again. The new input is passed to the component in the text property.

In a standalone skill conversation, the runtime displays the message, if provided, prompts the user for input, and then executes the component again. The new input is passed to the component in the text property.

This example code calls conversation.invalidInput(payload) whenever the input doesn’t convert to a number.

"use strict"
 
module.exports = {
 
    metadata: () => ({
        "name": "AgeChecker",
        "properties": {
            "minAge": { "type": "integer", "required": true }
        },
        "supportedActions": [
            "allow",
            "block",
            "unsupportedPayload"
        ]
    }),
 
    invoke: (conversation, done) => {
        // Parse a number out of the incoming message
        const text = conversation.text();
        var age = 0;
        if (text){
          const matches = text.match(/\d+/);
          if (matches) {
              age = matches[0];
          } else {
              conversation.invalidUserInput("Age input not understood. Please try again");
              done();
              return;
          }
        } else {
          conversation.transition('unsupportedPayload");
          done();
          return;
        }
 
        conversation.logger().info('AgeChecker: using age=' + age);
 
        // Set action based on age check
        let minAge = conversation.properties().minAge || 18;
        conversation.transition( age >= minAge ? 'allow' : 'block' );
 
        done();
    }
};

Here’s an example of how a digital assistant handles invalid input at runtime. For the first age response (twentyfive), there are no matches in any skills registered with the digital assistant so the conversation displays the specified conversation.invalidUserInput message. In the second age response (send money), the digital assistant finds a match so it asks if it should reroute to that flow.


Description of components-nonsequitur-conversation.png follows
Description of the illustration components-nonsequitur-conversation.png

You should call either conversation.invalidInput() or conversation.transition(). If you call both operations, ensure that the system.invalidUserInput variable is still set if any additional message is sent. Also note that user input components such as System.CommonResponse, System.Text, System.List, and System.ResolveEntities reset system.invalidUserInput.

Say, for example, that we modify the AgeChecker component as shown below, and call conversation.transition() after conversation.invalidInput().

if (matches) {  age = matches[0]; } else { 
      conversation.invalidUserInput("Age input not understood. Please try again"); 
      conversation.transition("invalid"); 
      conversation.keepTurn(true);
      done();
      return;
}

In this case, the data flow needs to transition back to askage so that the user gets two output messages – "Age input not understood. Please try again" followed by "How old are you?".

  askage:
    component: "System.Output"
    properties:
      text: "How old are you?"
    transitions: {}
  checkage:
    component: "AgeChecker"
    properties:
      minAge: 18
    transitions:
      actions:
        allow: "crust"
        block: "underage"
        invalid: "askage"

Run the Component Service in a Development Environment

During the development phase, you can start a local service to expose the custom component package.

  1. From the top-level folder, open a terminal window and run these commands to start the service:
    npm install
    npm start
  2. To verify that the service is running, enter the following URL in a browser:
    localhost:3000/components

    The browser displays the component metadata.

  3. If you have direct Internet access, you can access the development environment from a skill:
    1. Install a tunnel, such as ngrok or Localtunnel.
    2. If you are behind a proxy, go to http://www.whatismyproxy.com/ to get the external IP address of your proxy, and then, in the terminal window that you will use to start the tunnel, enter these commands:
      export https_proxy=http://<external ip>:80
      export http_proxy=http://<external ip>:80
    3. Start the tunnel and configure it to expose port 3000.
    4. In Oracle Digital Assistant, go to the skill's Components Components icon tab and add an External component service with the metadata URL set to https://<tunnel-url>/components.
      You can use any value for the user name and password.
You can now add states for the service's components to the dialog flow and test them from the skill tester.

Task 2: Deploy the Component Package to a Service

You can host the custom component package from a Digital Assistant embedded container, Mobile Hub, or an external Node.js server. For embedded component services, you deploy the package when you create the service. For external and Mobile Hub services, you must first deploy the package to the service, as described here, before you add it to a skill as a component service.

Deploy to Mobile Hub

To host a custom component package in Mobile Hub, use the bots-node-sdk pack --service mobile-api CLI to copy your component package folders and make a few changes that are specific to Mobile Hub, including the RAML file. Then create the custom API from the RAML file, and upload a ZIP of the component package into the custom API.

  1. From the custom component package's top-level folder (the one that contains the main.js file), type this command in a terminal window:

    bots-node-sdk pack --service mobile-api

    The command does the following:

    • Copies the files and subfolders to service-mobile-api-<package version>.
    • Adds a component.service.raml file, which contains the necessary endpoints and operations.
    • Creates an api.js file, which is a Mobile Hub wrapper for main.js.
    • Modifies the package.json file to set the main file to api.js, set the dependencies, and add the Mobile Hub node configuration.

    This step shows the basic CLI command. For more information, see https://github.com/oracle/bots-node-sdk/blob/master/bin/CLI.md.

  2. Review the package.json file and verify that the package name conforms to the Mobile Hub constraints:
    • The name must consist only of letters (A-Za-z), numbers (0-9), and underscores (_).
    • The name must begin with a letter.
    • The name must be 100 characters or less.
  3. From the Mobile Hub APIs page, click New API > API, and then create the custom API by uploading the component.service.raml file.

  4. From the Security tab, switch off Login Required and then click Save.

  5. Zip up the service-mobile-api-<package version> folder, and then upload the ZIP file from the custom API’s Implementation tab.

  6. From the Test page, invoke the GET request. The response should show the component metadata.

    Tip:

    If you get a status 500, and the error is that it can’t find a matching route definition, check your files for bad JavaScript syntax, as that is the typical cause.

Deploy to a Node.js Server

To host a custom component package on an external Node.js server, use the bots-node-sdk pack --service express CLI to copy your component package folders and make a few changes that are specific to Express, then install the component package and start it on your server.

  1. From the custom component package's top-level folder (the one that contains the main.js file), type this command in a terminal window:

    bots-node-sdk pack --service express

    The command does the following:

    • Copies the files and subfolders to service-express-<package version>.
    • Adds an index.js service wrapper.
    • Creates an api.js file, which is an Express wrapper for main.js.
    • Modifies the package.json file to set the main file to index.js and add the dependencies.

    This step shows the basic CLI command. For more information, see https://github.com/oracle/bots-node-sdk/blob/master/bin/CLI.md.

  2. Run these commands:

    npm install
    
    npm start

Task 3: Add Component Package to a Skill

You add a component package to a skill by creating a component service. For component packages that you host on an external server or on Oracle Mobile Hub, a component service is an active connection from the skill to host server. Alternatively, you can upload the component package and host it from your Oracle Digital Assistant instance. This is referred to as an embedded component service.

A component service has two functions:

  • It queries the component to get the package metadata, including the names of the components, their properties, and allowed actions for each one. After a service is added to the skill, you can see this information in the Components tab, which you access by clicking Components This is an image of the Components icon.in the left navbar. You can reference this page to get the component names, properties, and actions, which you will need to use the components in your dialog flow.

  • It allows the skill to invoke the components.

    The JSON payload of the call made by the Dialog Engine to the components includes input parameters, variable values, user-level context, and the user’s message text. The component returns the results by changing the values of existing variables or adding new ones (or both). The Dialog Engine parses the returned payload and proceeds.

To add a custom component package to a skill, go to the skill's Components This is an image of the Components icon.tab and click Add Service, which opens a dialog for configuring the service.

How you configure the service depends on where you are hosting the component package – Mobile Hub, your own Node.js server, or the Digital Assistant embedded container.

When you upload a package to the embedded container, Digital Assistant verifies that the package is valid, and can reject the package for these reasons:

  • There are JavaScript errors.

  • The package doesn't contain all the node module dependencies.

  • A component name has more than 100 characters, begins with System., or contains other than alphanumeric characters and underscores, then the service creation fails.

  • Your instance already has the maximum number of embedded component services.

  • The TGZ file is too large. This typically happens when the .npmignore file doesn't contain a *.tgz entry and therefore, every time you pack the files, a nested copy of the existing TGZ is added.

See Add Embedded Component Service for more information about these verification checks.

Add Embedded Component Service

If you want to host the custom component package from your Oracle Digital Assistant instance, complete these steps:

  1. Prepare the Package for an Embedded Container Service.

  2. Upload Package to Create an Embedded Component Service.

Prepare the Package for an Embedded Container Service

If you want to host the custom component package from Oracle Digital Assistant as an embedded component service, you must first pack the custom components into a TGZ file. Then, when you create the embedded component service, you upload this file.

This TGZ file, which you package using bots-node-sdk pack, must contain the assets and structure described in Task 1: Implement Custom Components. It also must contain all the node modules that it depends on (the bots-node-sdk pack does that for you).

Note:

There's a limit to how many embedded custom component services an instance can have. If you don't know the limit, ask your service administrator to get the embedded-custom-component-service-count for you as described in View Service Limits in the Infrastructure Console. If you try to add a component service after you meet that limit, then the service creation fails.

To prepare a package for uploading to the embedded container service:

  1. Ensure that you have the latest version of the Bots Node.js command line tools.

    The embedded container requires that the TGZ file includes all dependencies. Earlier versions did not bundle the dependencies into the file. Now, the command that you'll use to create the TGZ file ensures that your package.json file contains a bundledDependencies node that lists all the dependent modules that need to be included in the TGZ file.

  2. In the directory that contains the main.js file, run the following command for each of the modules that your package depends on. You don't need to do this for devDependencies, such as the Bots Node SDK.

    This command adds the module to the node_modules folder and adds it as a dependency in package.json.

    npm install <module>

    If your package.json already names all the dependencies, then you can run npm install instead.

  3. Ensure that the top-level folder contains an .npmignore file that has a *.tgz entry. For example:

    *.tgz
    spec
    service-*

    Otherwise, when you pack the files into a TGZ file, you include the TGZ file that already exists in the top-level folder, and your TGZ file will double in size. After you pack the files a few times, the TGZ file will be to large to upload into the container.

  4. Run this command:

    bots-node-sdk pack

    This command validates the component package, updates it to include devDependencies if necessary, and then creates a TGZ file, which you'll upload when you create an embedded component service from the skill’s Components tab. Note that the files you've listed as dependencies are included as bundledDependencies, with the exception of the Bots Node SDK and Express, which are devDependencies.

Your package should be compatible with the following versions:

  • For Oracle Digital Assistant instances that are provisioned on Oracle Cloud Infrastructure (sometimes referred to as the Generation 2 cloud infrastructure), your package should be compatible with Node.js 8.11.5 and Bots Node SDK 2.2.2.
  • For Oracle Digital Assistant instances that are provisioned on Oracle Cloud Platform (as all version 19.4.1 instances are), your package should be compatible with 8.11.4 and Bots Node SDK 2.2.0.

For more information about the pack command, see https://github.com/oracle/bots-node-sdk/blob/master/bin/CLI.md.

Upload Package to Create an Embedded Component Service

After you pack a custom component package into a TGZ file, you can upload it to create an embedded component service from the skill's Components tab.

To learn how to create the TGZ file, see Prepare the Package for an Embedded Container Service.

Note:

When you upload the package to the embedded component service, it's deployed to Oracle Functions Service. If your instance is provisioned on the Oracle Cloud Platform (as all version 19.4.1 instances are), then the service is instead deployed within the Digital Assistant instance.

To create the embedded component service:

  1. From the skill, click Components Components icon.

  2. Click + Service button..

  3. Select Embedded Container.

  4. Click Upload a component package file and point to the TGZ file to upload, or drag the file to the Package File box.

  5. (Optional) If you want to send custom component conversation.logger() statements to the service's log, then switch Enable Component Logging to On. This switch is available only in instances of Oracle Digital Assistant that were provisioned on Oracle Cloud Infrastructure (sometimes referred to as the Generation 2 cloud infrastructure).

    You can see the log from the Components tab by clicking View Logs.

  6. Click Create.

    Digital Assistant uploads the TGZ file and creates the embedded component service. During the upload, Digital Assistant verifies that the package is valid, and can reject the package for the reasons that are described later in this section.

    After you upload the TGZ file, the custom component service is built and its components are deployed. If the Components page displays an awaiting deployment message after you upload the TGZ file, it means that the service has been created, but is not yet available. When the service becomes available, the deployment metadata displays in place of the awaiting deployment message.

  7. Ensure that Service Enabled is switched to On.

During the upload, Digital Assistant might reject the package. Here are reasons for rejection and ways to resolve the issues.

  • JavaScript contains syntax errors: If a component's JavaScript has syntax errors, then that component is not added to the container, which results in this error message:

    Error Message: failed to start service built: Invalid component
          path:

    View the component files in an editor that detects syntax errors. Also, try hosting the package on a local server that sends error messages to a console log.

    Another reason for this message might be that the package doesn't contain all the node module dependencies. See the next item in this list.

  • Missing node modules: If the package doesn't contain all the node module dependencies, then you'll get the same error message as above:

    Error Message: failed to start service built: Invalid component
          path:

    To learn how to include node module dependencies, see Prepare the Package for an Embedded Container Service.

  • Component name is too long: If a component name has more than 100 characters, begins with System., or contains other than alphanumeric characters and underscores, then the service creation fails.

    Change the name in the component's JavaScript, repackage, and upload again.

  • Exceeded component service limit: If your instance already has the maximum number of embedded custom component services, then the service creation fails. Ask your service administrator for the embedded-custom-component-service-count limit as described in View Service Limits in the Infrastructure Console.

    If you need to raise the limit, you can request an increase. See Requesting a Service Limit Increase.

  • TGZ file is too large: This typically happens when the .npmignore file doesn't contain a *.tgz entry and therefore, every time you pack the files, a nested copy of the existing TGZ is added.

    When the top-level folder contains an .npmignore file with *.tgz, the previous version of the TGZ file isn't included when you update the package.

If you want to send custom component conversation.logger() statements to the service's log, then switch Enable Component Logging to On. This switch is available only in instances of Oracle Digital Assistant that were provisioned on Oracle Cloud Infrastructure (sometimes referred to as the Generation 2 cloud infrastructure).

When Enable Component Logging is switched to On, you can click the Diagnostics button for the service to access view logs and crash reports to diagnose the problem.

  • Select View Logs to view messages that the custom component sends to conversation.logger(). This feature is available only in instances of Oracle Digital Assistant that were provisioned on Oracle Cloud Infrastructure (sometimes referred to as the Generation 2 cloud infrastructure). The Enable Component Logging switch must be On for the log to contain these messages.
  • Select View Crash Report to view details about what may have caused the container to crash.

Add Mobile Hub Component Service

You can host your custom components from Oracle Mobile Hub, and add them to a skill as an Oracle Mobile Cloud component service. Custom components that are hosted on Mobile Hub can integrate with remote services using connectors that are controlled by a Mobile Hub backend and they have access to the Mobile Hub platform APIs.

Because the backend that hosts the custom code handles the authentication for the custom components, you need to refer to the backend’s Settings page to get the information that you need to complete the configuration.

To add a component service for the Mobile Hub backend:

  1. From the skill, click Components Components icon.

  2. Click + Service button..

  3. Select Oracle Mobile Cloud.

  4. Enter the unique identifier assigned to the Mobile Hub backend in the Backend ID field. This ID is passed in the REST header of every call from the skill.

  5. In the MetadataURL field, enter the /components endpoint from the custom code API. For example, http://<server>:<port>/mobile/custom/ccPackage/components.

  6. Choose Use Anonymous Access if the service allows anonymous login. If you choose this option, enter the anonymous key, which is a unique string that allows your app to access anonymous APIs without sending an encoded user name and password. The anonymous key is passed in their place. You can find the anonymous key on the backend's Settings page in Mobile Hub. (You may need to click Show.)

    If the component service requires a login (meaning no anonymous access), then enter the user name and password.

  7. If the service requires specific parameters, click Add HTTP Header and then define the key-value pairs for the headers.

  8. Click Create.

  9. Ensure that Service Enabled is switched to On.

Add External Component Service

You can host your custom components on your own Node.js server and add them to a skill as an external component service.

Tip:

You can use the external service option during development, as described in Run the Component Service in a Development Environment.

To add an external component service:

  1. From the skill, click Components Components icon.

  2. Click + Service button..

  3. Select External.

  4. In the Metadata URL text box, enter the The URL that points to the GET endpoint that returns the list of components.

  5. Enter the service's user name and password.

  6. Click Create.

  7. Ensure that Service Enabled is switched to On.