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, Oracle Cloud Infrastructure
Functions, a Mobile Hub backend, or a Node.js server:
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.
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
On Windows, the Bots Node SDK doesn't work on
Windows if the Node installation is version 20.12.2 or higher because of a
backward-incompatible change in Node.js. If you already have Node version 20.12.2 or
higher installed, you need to uninstall it and then install version 20.12.1 or
earlier version for the Bots Node SDK to work.
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
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.
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 🔗
To start a project, you use the bots-node-sdk init command from the SDK’s command line interface (CLI) to create the necessary files and directory structure for your component structure.
The init command has a few options, such as whether to use JavaScript (the default) or TypeScript, and what to name the initial component's JavaScript file. These options are described in CLI Developer Tools. Here's the basic command for starting a JavaScript project:
bots-node-sdk init <top-level folder path> --name <component service name>
This command completes the following actions for a JavaScript package:
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.
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 Node 14.17.0.
Step 3: Create and Build a Custom Component 🔗
Here are the steps for building each custom component in your package:
Use the SDK's CLI init component command to create a JavaScript or TypeScript file with the framework for working with the Oracle Digital Assistant Node.js SDK to write a custom component. The language that you specified when you ran the init command to create the component package determines whether a JavaScript or a TypeScript file is created.
For example, to create a file for the custom component, from a terminal window, CD to the package’s top-level folder and type the following command, replacing <component name> with your component's name:
bots-node-sdk init component <component name> c components
For JavaScript, this command adds the <component name>.js to the components folder. For TypeScript, the file is added to the src/components folder. The c argument indicates that the file is for a custom component.
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.
metadata: This provides the following component information to the
skill.
Component name
Supported properties
Supported transition actions
For YAML-based dialog flows, the custom component supports the
following properties by default. These properties aren't available for skills designed in
Visual dialog mode.
insightsEndConversation: Boolean. Not required. When
true, the session stops recording the conversation for insights
reporting. The default is false. See Model the Dialog Flow.
insightsInclude: Boolean. Not required. When
true, the state is included in insights reporting. The default is
true. See Model the Dialog Flow.
translate: Boolean. Not required. When
true, autotranslation is enabled for this component. The default is
the value of the autotranslation context variable. See Translation Services in Skills.
invoke: This contains the logic to execute. In this method, you can
read and write skill context variables, create conversation messages, set state transitions,
make REST calls, and more. Typically, you would use the async keyword with
this function to handle promises. The invoke function takes the following
argument:
context, which names the reference to the
CustomComponentContext object in the Digital Assistant Node.js SDK. This class is described in the SDK documentation at https://oracle.github.io/bots-node-sdk/. In earlier versions of the
SDK, the name was conversation. You can use either name.
Note
If you are using a JavaScript
library that doesn't support promises (and thus aren't using async
keyword), it is also possible to add a done argument as a callback that
the component invokes when it has finished processing.
Here’s an example:
'use strict';
module.exports = {
metadata: {
name: 'helloWorld',
properties: {
human: { required: true, type: 'string' }
},
supportedActions: ['weekday', 'weekend']
},
invoke: async(context) => {
// Retrieve the value of the 'human' component property.
const { human } = context.properties();
// determine date
const now = new Date();
const dayOfWeek = now.toLocaleDateString('en-US', { weekday: 'long' });
const isWeekend = [0, 6].indexOf(now.getDay()) > -1;
// Send two messages, and transition based on the day of the week
context.reply(`Greetings ${human}`)
.reply(`Today is ${now.toLocaleDateString()}, a ${dayOfWeek}`)
.transition(isWeekend ? 'weekend' : 'weekday');
}
}
To learn more and explore some code examples, see Writing Custom Components in the Bots Node SDK
documentation.
Control the Flow with keepTurn and transition 🔗
You use different combinations of the Bots Node SDK keepTurn and
transition functions 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(), 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.
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.
If applicable, use context.reply(<reply>) to send a reply.
Set context.keepTurn(true).
Set context.transition with either a supportedActions string (e.g., context.transition("success")) or with no argument (e.g., context.transition()).
For example, this custom component updates a variable with a list of values to be
immediately displayed by the next state in the dialog
flow.
invoke: async (context) => {
const listVariableName = context.properties().variableName;
...
// Write list of options to a context variable
context.variable(listVariableName, list);
// Navigate to next state without
// first prompting for user interaction.
context.keepTurn(true);
context.transition();
}
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.
If applicable, use context.reply(<reply>) to send a reply.
Set context.keepTurn(false) .
Set context.transition with either a supportedActions string(context.transition("success")) or with no arguments (context.transition()).
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 context.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.
Do not call transition.
Set keepTurn(false).
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: async (context) => {
// Perform conversation tasks.
const tracking_token = "a2VlcHR1cm4gZXhhbXBsZQ==";
const quotes = require("./json/Quotes.json");
const 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 (context.postback() && context.postback().token == tracking_token && context.postback().isNo) {
context.keepTurn(true);
context.transition();
} else {
// Show the quote of the day.
context.reply("'" + quote.quote + "'");
context.reply(" Quote by: " + quote.origin);
// Create a single message with two buttons to
// request another quote or not.
const mf = context.getMessageFactory();
const message = mf.createTextMessage('Do you want another quote?')
.addAction(mf.createPostbackAction('Yes', { isNo: false, token: tracking_token }))
.addAction(mf.createPostbackAction('No', { isNo: true, token: tracking_token }));
context.reply(message);
// 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.
context.keepTurn(false);
};
}
If a component doesn’t transition to another state, 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: context.variable("InternalComponentWaitTime", time). If you use a context variable, don't forget to reset it or set it to null before calling context.transition.
Note that as long as you don't transition, all values that are passed in as
component properties are available.
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 context.keepTurn(true).
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: async (context) => {
const quotes = require("./json/Quotes.json");
const quote = quotes[Math.floor(Math.random() * quotes.length)];
// Check if postback action is issued and postback is from this component rendering.
// This ensures that the component only responds to its own postback actions.
const um = context.getUserMessage()
if (um instanceof PostbackMessage && um.getPostback() && um.getPostback()['system.state'] === context.getRequest().state && um.getPostback().isNo) {
context.keepTurn(true);
context.transition();
} else {
// Show the quote of the day.
context.reply(`'${quote.quote}'`);
context.reply(`Quote by: ${quote.origin}`);
// Create a single message with two buttons to request another quote or not.
let actions = [];
const mf = context.getMessageFactory();
const message = mf.createTextMessage('Do you want another quote?')
.addAction(mf.createPostbackAction('Yes', { isNo: false }))
.addAction(mf.createPostbackAction('No', { isNo: true }));
context.reply(message);
// 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.
context.keepTurn(false);
}
}
Access the Backend 🔗
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. We recommend that you use a library that supports promises so that you can leverage the async version of the invoke method, which was introduced in version 2.5.1, and use the await keyword to write your REST calls in a synchronous way.
Use the SDK to Access Request and Response Payloads 🔗
You use CustomComponentContext instance methods to get the context for the invocation, access and change variables, and send results back to the dialog engine.
When you design a custom component, you should consider whether the component will be used by a skill that supports more than one language.
If the custom component must support multi-language skills, then you need to know if the skills are configured for native language support or translation service.
When you use a translation service, you can translate the text from the skill. You have these options:
For native language skills, you have these options:
Pass the data back to the skill in variables and then output the text from a system
component by passing the variables' values to a resource bundle key, as described in Use a System Component to Reference a Resource Bundle. With this option, the custom component must have metadata properties for
the skill to pass the names of the variables to store the data in.
Use the resource bundle from the custom component to compose the custom component's reply, as described in Reference Resource Bundles from the Custom Component. You use the conversation.translate() method to get the resource bundle string to use for your call to context.reply(). This option is only valid for resource bundle definitions that use positional (numbered) parameters. It doesn't work for named parameters. With this option, the custom component must have a metadata property for name of the resource bundle key, and the named resource bundle key's parameters must match those used in the call to context.reply().
Here's an example of using the resource bundle from the custom component. In this example, fmTemplate would be set to something like ${rb('date.dayOfWeekMessage', 'lundi', '19 juillet 2021')}.
'use strict';
var IntlPolyfill = require('intl');
Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;
module.exports = {
metadata: () => ({
name: 'Date.DayOfWeek',
properties: {
rbKey: { required: true, type: 'string' }
},
supportedActions: []
}),
invoke: (context, done) => {
const { rbKey } = context.properties();
if (!rbKey || rbKey.startsWith('${')){
context.transition();
done(new Error('The state is missing the rbKey property or it uses an invalid expression to pass the value.'));
}
//detect user locale. If not set, define a default
const locale = context.getVariable('profile.locale') ?
context.getVariable('profile.locale') : 'en-AU';
const jsLocale = locale.replace('_','-');
//when profile languageTag is set, use it. If not, use profile.locale
const languageTag = context.getVariable('profile.languageTag')?
context.getVariable('profile.languageTag') : jslocale;
/* =============================================================
Determine the current date in local format and
the day name for the locale
============================================================= */
var now = new Date();
var dayTemplate = new Intl.DateTimeFormat(languageTag,
{ weekday: 'long' });
var dayOfWeek = dayTemplate.format(now);
var dateTemplate = new Intl.DateTimeFormat(languageTag,
{ year: 'numeric', month: 'long', day: 'numeric'});
var dateToday = dateTemplate.format(now);
/* =============================================================
Use the context.translate() method to create the ${Freemarker}
template that's evaluated when the reply() is flushed to the
client.
============================================================= */
const fmTemplate = context.translate(rbKey, dateToday, dayOfWeek );
context.reply(fmTemplate)
.transition()
.logger().info('INFO : Generated FreeMarker => '
+ fmTemplate);
done();
}
};
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 context.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 context.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: (context, done) => {
// Parse a number out of the incoming message
const text = context.text();
var age = 0;
if (text){
const matches = text.match(/\d+/);
if (matches) {
age = matches[0];
} else {
context.invalidUserInput("Age input not understood. Please try again");
done();
return;
}
} else {
context.transition('unsupportedPayload");
done();
return;
}
context.logger().info('AgeChecker: using age=' + age);
// Set action based on age check
let minAge = context.properties().minAge || 18;
context.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 context.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.
You should call either context.invalidInput() or
context.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 Common Response and
Resolve Entities components) reset system.invalidUserInput.
Say, for example, that we modify the AgeChecker component as shown below, and call context.transition() after context.invalidInput().
if (matches) { age = matches[0]; } else {
context.invalidUserInput("Age input not understood. Please try again");
context.transition("invalid");
context.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?". Here's how that might be handled in a YAML-mode dialog
flow.
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:
Start the tunnel and configure it to expose port 3000.
In Oracle Digital Assistant, go to the skill's Components 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's Preview page.