Typescript / Javascript library to extend the Cognigy.AI 4
platform with your own code.
- Table of Contents
- Installing
- Folder Structure
- Example
- Usage
Using npm:
npm i @cognigy/extension-tools
Using yarn:
yarn add @cognigy/extension-tools
- extension-name
- README.md
- src/
- nodes/
- myFlowNode.ts
- connections/
- myConnection.ts
- module.ts
- package.json
- package-lock.json
- tslint.json
- tsconfig.json
- icon.png
- nodes/
This structure includes all the required resources to build and upload the Extension to Cognigy.AI. The README.md is used to show the code's content. Thus, other developers or interested users can get familiar with the functionality. The entire source code of the exposed Flow Nodes is located in the src/nodes/
folder, in which the next four files (package.json, package-lock.json, tslint.json, tsconfig.json) are used to create the Javascript module and check it for mistakes. The icon is required to show the module's logo in Cognigy.AI. In most cases, the icon shows the logo of the third-party software product which we are integrating with.
Notes:
- The icon.png needs to have the following dimensions: 64x64 Pixels
In order to use the provided interfaces and functions, the NPM package needs to be imported to every file:
import { createExtension } from "@cognigy/extension-tools";
import { IConnectionSchema } from "@cognigy/extension-tools";
import { createNodeDescriptor, INodeFunctionBaseParams } from "@cognigy/extension-tools";
A Connection can be created by using the following format, while the list of fields define the list of required credential values the user needs to provide inside of Cognigy.AI:
import { IConnectionSchema } from "@cognigy/extension-tools";
export const myConnection: IConnectionSchema = {
type: "myconnection",
label: "Basic Auth",
fields: [
{ fieldName: "username" },
{ fieldName: "password" }
]
};
If the Extension should provide multiple Connections, e.g. Basic Auth, OAuth2 and API Key, each Connection is defined in its own file:
- connections/
- basicAuthConnection.ts
- oauthConnection.ts
- apiKeyConnection.ts
An Extension Flow Node is created by describing fields
, sections
and function
, for example, while this package provides a method called createNodeDescriptor()
in order to follow the necessary interfaces:
import { createNodeDescriptor, INodeFunctionBaseParams } from "@cognigy/extension-tools";
export interface IMyParams extends INodeFunctionBaseParams {
config: {};
}
export const myFlowNode = createNodeDescriptor({
type: "myFlowNode",
defaultLabel: "My Flow Node",
fields: [],
function: async ({ cognigy, config }: IMyParams) => {}
});
This minimal setup now can be filled with information:
import { createNodeDescriptor, INodeFunctionBaseParams } from "@cognigy/extension-tools";
export interface IMyParams extends INodeFunctionBaseParams {
config: {
text: string;
};
}
export const myFlowNode = createNodeDescriptor({
type: "myFlowNode",
defaultLabel: "My Flow Node",
fields: [
{
key: "text",
label: "Text",
type: "cognigyText",
defaultValue: "{{input.text}}",
params: {
required: true
}
},
],
function: async ({ cognigy, config }: IMyParams) => {
const { api } = cognigy;
const { text } = config;
api.say(`You said ${text}`);
}
});
Next to this pseudo-code example, one can find more examples in the GitHub Extensions Repository.
If the Extension should provide multiple Flow Nodes, each Node is defined in its own file:
- nodes/
- createContact.ts
- getContact.ts
- search.ts
Finally, the Extension can be created with the described Flow Nodes and Connections. For this purpose, this package provides a function called createExtension()
:
import { createExtension } from "@cognigy/extension-tools";
import { myConnection } from "./connections/myConnection";
import { myFlowNode } from "./nodes/myFlowNode";
export default createExtension({
nodes: [
myFlowNode
],
connections: [
myConnection
]
});
Instead of showing all configured Fields among themselves, they can be organized into sections using the sections
and form
properties, improving the Node Editor's user experience.
In this example, the Node's form will be structured like this:
- Field 1
- Section 1
- Field 2
- Field 3
- Section 2
- Field 4
- Field 5
createNodeDescriptor({
// ...
fields: [
// ... (field1, field2, field3, field4, field5)
],
sections: [
{
key: "section1",
label: "Section 1",
fields: ["field1", "field2"]
},
{
key: "section2",
label: "Section 2",
fields: ["field3", "field4"]
}
],
form: [
{
key: "field1",
type: "field"
},
{
key: "section1",
type: "section"
},
{
key: "section2",
type: "section"
}
]
});
It's possible to customize the appearance of a Node in order to improve recognizability of the Node's type and content configuration in the Flow Editor.
In this example, the Flow Node will have a light pink color with dark text on it, and the second line of the Flow will preview the value configured in the field1
field.
createNodeDescriptor({
// ... (field1)
appearance: {
color: "#FFAABB",
textColor: "#222222"
},
preview: {
key: "field1",
type: "text"
}
});
A Node can introduce new "Cognigy Tokens" to the Flow Editor when the Extension is installed by defining the tokens
section. This can be useful when the Node writes data e.g. into the conversation context
that can then easilly accessed through a graphical token in the CognigyScript editors.
The following Node grants easy access to its result value by a Cognigy Token called Some Node Result
that can be used e.g. in Say Nodes.
createNodeDescriptor({
// ...
function: async ({ cognigy }) => {
// ...
cognigy.context.someNodeResult = "example result";
},
tokens: [
{
type: "context",
label: "Some Node Result",
script: "context.someNodeResult"
}
]
});
It is possible to conditionally show Fields and Sections based on other parts of the Node's configuration in order to improve the user experience for Flow builders.
Fields and Sections with their condition
property configured will only be visible in the Node Editor UI if their configured condition is met.
In this example, the field field2
is only shown if the value of field1
is true
.
createNodeDescriptor({
// ...
fields: [
{
key: "field1",
type: "toggle",
// ...
defaultValue: false,
},
{
key: "field2",
// ...
condition: {
key: "field1",
value: true
}
}
]
});
In this example, field2
will only be shown if the value of field1
is "example"
.
createNodeDescriptor({
// ...
fields: [
{
key: "field1",
type: "text",
// ...
},
{
key: "field2",
// ...
condition: {
key: "field1",
value: "example"
}
}
]
});
In this example, field2
will only be shown if the value of field1
is either "example1"
or "example2"
.
createNodeDescriptor({
// ...
fields: [
{
key: "field1",
type: "text",
// ...
},
{
key: "field2",
// ...
condition: {
key: "field1",
value: ["example1", "example2"]
}
}
]
});
In this example, the section1
section and all its nested fields are only shown if the value of field1
is true
.
createNodeDescriptor({
// ...
fields: [
// ... (field1)
],
sections: [
{
key: "section1",
// ...
condition: {
key: "field1",
value: true
}
}
]
});
In this example, field2
will only be shown if the value of field1
is not true
.
createNodeDescriptor({
// ...
fields: [
// ... (field1)
{
key: "field2",
// ...
condition: {
key: "field1",
value: true,
negate: true
}
}
]
})
In this example, field3
is only shown if the values of both field1
and field2
are set to true
.
createNodeDescriptor({
// ...
fields: [
// ... (field1 and field2)
{
key: "field3",
// ...
condition: {
and: [
{
key: "field1",
value: true
},
{
key: "field2",
value: true
}
]
}
}
]
});
In this example, field3
is only shown if at least one of the values of field1
or field2
is set to true
.
createNodeDescriptor({
// ...
fields: [
// ... (field1 and field2)
{
key: "field3",
// ...
condition: {
or: [
{
key: "field1",
value: true
},
{
key: "field2",
value: true
}
]
}
}
]
});
In this example, field4
is only shown either if field1
and field2
are both set to true
or if field3
is set to true
.
createNodeDescriptor({
// ...
fields: [
// ... (field1, field2 and field3)
{
key: "field4",
// ...
condition: {
or: [
{
and: [
{
key: "field1",
value: true
},
{
key: "field2",
value: true
}
]
},
{
key: "field3",
value: true
}
]
}
}
]
});
You can build decision-making and branching techniques using Child Nodes and the setNextNode
API. This is e.g. used in the Lookup
and If
Nodes.
The example below shows how to build a random
Node and its exclusive Child Node type, randomChild
. When executed, it picks one of its children at random and continues Flow execution at that Child Node.
createNodeDescriptor({
// ...
type: "random",
// pick a random configuration from one of its child Nodes
// and continue Flow execution there
function: async ({ childConfigs, cognigy }) => {
if (childConfigs.length > 0) {
const nextNode = childConfigs[Math.floor(Math.random() * childConfigs.length)]
cognigy.api.setNextNode(nextNode.id);
}
},
// only "randomChild" Nodes are allowed as children for this Node!
constraints: {
placement: {
children: {
whitelist: ["randomChild"]
}
}
},
// when this Node is created, two "randomChild" child Nodes
// will be created automatically
dependencies: {
children: ["randomChild", "randomChild"]
}
});
createNodeDescriptor({
// ...
type: "randomChild",
parentType: "random",
appearance: {
variant: "mini"
}
});
Creating Extensions is an important feature required to further extend Cognigy AI with additional functionality. As the UI supports several languages in the menus we also added a functionality to add translations to Extensions.
Localization is a completely optional feature that can be used whenever required. You don't need to enforce it consistently throughout an Extension, you can use it as well to translate just a single string.
Localization was introduced with Cognigy AI version 4.12.0, therefore an upgrade to this or a later version is required to use this feature.
Localization supports the replacement of simple strings with a JSON object like this:
{
"default": "Default fallback string",
"enUS": "String with English Localization",
"deDE": "String with German Localization",
"esES": "String with Spanish Localization",
"koKR": "String with Korean Localization",
"jaJP": "String with Japanese Localization"
}
The default
property is mandatory and the other ones are optional, so you can e.g. choose to have an English and a German Localization only.
Localization will be visible for every user of the Cognigy.AI User Interface (UI), therefore a Node with a German Localization will be displayed in German for every user that has their UI set to German.
End users won't see a difference when communicating with the localized Node.
Localization doesn't work for all properties. Right now these properties are supported in a Descriptor:
defaultLabel
summary
These properties are supported in a Node Section:
label
These properties are supported in a Node Field:
label
description
If a Node Field is of type "select" then it's also supported in the label property of the options in the params
Let's take a sample Descriptor built without Localization:
export const mySampleNode = createNodeDescriptor({
type: "myExampleNode",
defaultLabel: "My example Node",
summary: "Just a simple example Node",
fields: [
{
key: "textInput",
label: "My Example Text Input"
type: "cognigyText",
},
{
key: "selectIcecream",
label: "Do you like Icecream?"
params: {
options: [
{
label: "Yes, I love Icecream!",
value: true
},
{
label: "No, I don't",
value: false
}
]
}
}
],
sections: [
{
key: "importantQuestions",
label: "Important Questions",
defaultCollapsed: true,
fields: [
"selectIcecream"
]
}
],
form: [
{ type: "field", key: "textInput" },
{ type: "section", key: "importantQuestions" }
],
function: async ({ cognigy, config }: IMySampleNode) => {
// my Node logic
}
})
adding a German translation might look like this:
export const mySampleNode = createNodeDescriptor({
type: "myExampleNode",
defaultLabel: {
default: "My example Node",
deDE: "Mein Beispiel Node"
},
summary: {
default: "Just a simple example Node",
deDE: "Nur ein einfaches Beispielnode"
},
fields: [
{
key: "textInput",
label: {
default: "My Example Text Input",
deDE: "Mein Beispiel Text Eingabefeld"
},
type: "cognigyText",
},
{
key: "selectIcecream",
label: {
default: "Do you like Icecream?",
deDE: "Magst du Eiscreme?"
},
params: {
options: [
{
label: {
default: "Yes, I love Icecream!",
deDE: "Ja, ich liebe Eiscreme!"
},
value: true
},
{
label: {
default: "No, I don't",
deDE: "Nein, ich mag es nicht"
},
value: false
}
]
}
}
],
sections: [
{
key: "importantQuestions",
label: {
default: "Important Questions",
deDE: "Wichtige Fragen"
},
defaultCollapsed: true,
fields: [
"selectIcecream"
]
}
],
form: [
{ type: "field", key: "textInput" },
{ type: "section", key: "importantQuestions" }
],
function: async ({ cognigy, config }: IMySampleNode) => {
// my Node logic
}
})
Options Resolvers are a way to enhance the seamless integration of a Cognigy Extension with third-party systems by providing dynamically resolved select options for Node Fields.
Options Resolvers were introduced with Cognigy AI version 4.9.0, therefore an upgrade to this or a later version is required to use this feature.
The benchmark case this feature was built around was having the Flow-editing users pick an actual entity on a third-party system (e.g. a file on their Dropbox) It could also be used without third-party systems e.g. to filter a set of available options based on other field selections (e.g. a list of cities that can be shrunk down to only show cities from a certain country in case another "country" field was set).
The following code snippet describes a "field configuration" featuring an Options Resolver that causes the select field to display a list of files as options which were fetched from a third-party API.
It assumes that there is an HTTP API at https://example.service/files
that returns a list of "files" as JSON and requires an authentication header to be set.
The example also assumes that there is another field called credentials
in this Node. It would typically be a "secret".
The "Resolver Function" fetches a list of files from the API using the value of the credentials
field and returns it as an array of options.
const node = createNodeDescriptor({
// ...
fields: [
// ...
{
type: "select",
key: "file",
label: "File on Example Service",
optionsResolver: {
dependencies: ["credentials"],
resolverFunction: async ({ api, config }) => {
// fetch list of files using http request
const response = await api.httpRequest({
method: "GET",
url: `https://example.service/files`,
headers: {
xApiKey: config.credentials.apiKey,
},
});
// map file list to "options array"
return response.map((file) => {
return {
label: file.basename,
value: file.path,
};
});
},
},
},
],
});
The optionsResolver
configuration can only be set on Node Fields with type select
so far.
The dependencies
array contains a list of all Node field values that are necessary for the resolverFunction
. Typically, this would include a key to a "Secret Field" where authentication credentials for the API are available.
All registered dependency Field values will be available to the resolverFunction
as an object mapping ({ [fieldKey]: fieldValue }
).
The resolverFunction
will be called in order to resolve new options for the select field. It will be triggered from the Node Editor, but executed in the backend.
The resolverFunction
has access to a config
object containing a key-value-mapping of all registered dependency Fields (see section above).
Every time a change to a registered dependency Field is made in the Node Editor, the resolverFunction
will be triggered with the new values (including intermediate, non-saved changes!).
The resolverFunction
has to return an array of objects with the exact shape of:
interface IResolvedOption {
label: string;
value: string;
}
The returned value of the resolverFunction
will be validated using a schema. If it does not match the "Option Schema" described above, it will not return any options.
The resolverFunction
will get an api
object as a parameter.
You will be able to perform an HTTP request using api.httpRequest
.
The httpRequest
function will respect the proxy configuration that is configured for the Cognigy.AI installation you are running on.