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, 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.
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.
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 namedhello.world.js
. This is where you'll put your component JavaScript files. -
Adds a
package.json
file, which specifiesmain.js
as the main entry point and lists@oracle/bots-node-sdk
as adevDependency
. The package file also points to somebots-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.
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:
Create the Component File
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.
For further details, see https://github.com/oracle/bots-node-sdk/blob/master/bin/CLI.md
.
Add Code to the metadata and invoke Functions
Your custom component must export two objects:
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.
autoNumberPostbackActions
: Boolean. Not required. Whentrue
, buttons and list options are numbered automatically. The default isfalse
. See Auto-Numbering for Text-Only Channels in YAML Dialog Flows.insightsEndConversation
: Boolean. Not required. Whentrue
, the session stops recording the conversation for insights reporting. The default isfalse
. See Model the Dialog Flow.insightsInclude
: Boolean. Not required. Whentrue
, the state is included in insights reporting. The default istrue
. See Model the Dialog Flow.translate
: Boolean. Not required. Whentrue
, autotranslation is enabled for this component. The default is the value of theautotranslation
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 theasync
keyword with this function to handle promises. Theinvoke
function takes the following argument:context
, which names the reference to theCustomComponentContext
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 wasconversation
. You can use either name.
Note
If you are using a JavaScript library that doesn't support promises (and thus aren't usingasync
keyword), it is also possible to add adone
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 callkeepTurn
after you callreply
becausereply
implicitly setskeepTurn
tofalse
. -
transition(action)
causes the dialog to transition to the next state after all replies, if any, are sent. The optionalaction
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.
invoke: async (context) ==> {
...
context.reply(payload);
context.keepTurn(true);
context.transition ("success");
}
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. |
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.
|
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. |
For
example:
|
A custom component that gets user input without returning flow control back to the skill. For example:
|
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 .
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: 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:
|
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:
|
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.
One option is the node fetch API that's pre-installed with the Bots Node SDK. Access the Backend Using HTTP REST Calls in the Bots Node SDK documentation contains some code examples.
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.
You can find several code examples for using these methods in Writing Custom Components and Conversation Messaging in the Bots Node SDK documentation
The SDK reference documentation is at https://github.com/oracle/bots-node-sdk
.
Custom Components for Multi-Language Skills
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:
-
Set the
translate
property in the custom component's state to true to translate the component's reply, as described in Send Responses Directly to the Translation Service. -
Send raw data back to the skill in variables and use the variables' values in a system component that composes the output. Set that component's
translate
property to true. See Use a System Component to Pass the Message to the Translation Service. -
Send raw data back to the skill in variables and use the variables' values in a system component that uses the resource bundle key for the language. See Use a System Component to Reference a Resource Bundle.
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 tocontext.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 tocontext.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.
askage:
component: "System.Output"
properties:
text: "How old are you?"
transitions:
next: "checkage"
checkage:
component: "AgeChecker"
properties:
minAge: 18
transitions:
actions:
allow: "crust"
block: "underage"
invalid: "askage"