@cognigy/extension-tools
    TypeScript icon, indicating that this package has built-in type declarations

    0.14.0 • Public • Published

    Typescript / Javascript library to extend the Cognigy.AI 4 platform with your own code.

    Table of Contents

    Installing

    Using npm:

    npm i @cognigy/extension-tools

    Using yarn:

    yarn add @cognigy/extension-tools

    Folder Structure

    • extension-name
      • README.md
      • src/
        • nodes/
          • myFlowNode.ts
        • connections/
          • myConnection.ts
        • module.ts
        • package.json
        • package-lock.json
        • tslint.json
        • tsconfig.json
        • icon.png

    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

    Example

    Import extension-tools

    In order to use the provided interfaces and functions, the NPM package needs to be imported to every file:

    module.ts

    import { createExtension } from "@cognigy/extension-tools";

    connection.ts

    import { IConnectionSchema } from "@cognigy/extension-tools";

    flowNode.ts

    import { createNodeDescriptor, INodeFunctionBaseParams } from "@cognigy/extension-tools";

    Connection

    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

    Flow Node

    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

    Module / Extension

    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
    	]
    });

    If no Connection is required in this Extension, the connections list can be removed:

    export default createExtension({
    	nodes: [
    		myFlowNode
    	]
    });

    Interface Descriptions

    Next to the minimum examples presented above, this package provides various settings, such as sections, form, appearance or dependencies, for example, that could be used for improving the Extension's usage experience and performance.

    createExtension()

    interface ICreateExtensionParams {
    	nodes: INodeDescriptor[];
    	connections?: IConnectionSchema[];
    	options?: IExtensionOptions;
    }

    connections

    export interface IConnectionSchema {
    	type: string;
    	label: string;
    	fields: IConnectionSchemaField[];
    }
    
    export interface IConnectionSchemaField {
    	/** The field name, e.g. 'client_id' */
    	fieldName: string;
    }

    options

    export interface IExtensionOptions {
    	label?: string;
    }

    nodes

    export interface INodeDescriptor {
    	/* The type of this descriptor, e.g. 'say' */
    	type: string;
    
    	/* The type of the node parent (if there is one) */
    	parentType?: string;
    
    	/* The label that should be used when a new Node of this type is created */
    	defaultLabel: string;
    
    	/* A short line of text that describes what this Node is used for */
    	summary?: string;
    
    	/* Appearance information for the node */
    	appearance: INodeAppearance;
    
    	/* Additionall behavioural information */
    	behavior?: INodeBehavior;
    
    	/* Constraints for how this node can be created, placed */
    	constraints?: INodeConstraints;
    
    	/* Dependencies which we want to create when this node is created  */
    	dependencies?: INodeDependencies;
    
    	/* The individual fields (arguments) of this node */
    	fields?: INodeField[];
    
    	/* The code of this node */
    	function?: TNodeFunction;
    
    	/** Defines how the preview should be generated for nodes using this descriptor */
    	preview?: INodePreview;
    
    	/** 
    	 * Tags which allow searching individual Nodes 
    	 * 
    	 * The following tags will put the Node
    	 * into the distinct "function tabs":
    	 *
    	 * - basic
    	 * - logic
    	 * - message
    	 * - profile
    	 * - service
    	 * - nlu
    	 * - data
    	 * */
    	tags?: TNodeTagType[];
    
    	/** Definition of tokens this node might fill */
    	tokens?: ISnippet[];
    
    	/** Sections which allow to group multiple fields */
    	sections?: INodeSection[];
    
    	/** The form defines how fields and sections should be render in order */
    	form?: INodeFieldAndSectionFormElement[];
    }

    appearance

    export interface INodeAppearance {
    	color?: string;
    	textColor?: string;
    	showIcon?: boolean;
    	variant?: "regular" | "mini" | "hexagon";
    }

    behavior

    export interface INodeBehavior {
    	stopping?: boolean;
    	entrypoint?: boolean;
    }

    constraints

    export interface INodeConstraints {
    	/* Should this node be editable? */
    	editable?: boolean;
    
    	/* Should this node be deletable? */
    	deletable?: boolean;
    
    	/* Should this node be manually creatable */
    	creatable?: boolean;
    
    	/* Should this node be callapsable */
    	collapsable?: boolean;
    
    	/* Should this node be movable */
    	movable?: boolean;
    
    	/* Additional placement information */
    	placement: {
    		predecessor?: INodeConstraint;
    		successor?: INodeConstraint;
    		children?: INodeConstraint;
    	};
    }

    dependencies

    export interface INodeDependencies {
    	/** A list of Node types */
    	children: string[];
    }

    fields

    An Extension Flow Node field is of type INodeFieldType:

    export interface INodeField {
    	type: TNodeFieldType;
    	key: string;
    	label: string;
    	condition?: TNodeFieldCondition;
    	defaultValue?: any;
    	description?: string;
    	params?: object;
    	optionsResolver: TOptionsResolver
    }

    while the following list of types can be used:

    [
    	"text",
    	"rule",
    	"json",
    	"checkbox",
    	"time",
    	"date",
    	"datetime",
    	"select",
    	"typescript",
    	"xml",
    	"textArray",
    	"chipInput",
    	"cognigyText",
    	"toggle",
    	"slider",
    	"number",
    	"daterange",
    	"connection",
    	"say",
    	"condition"
    ]
    condition

    There are three different types of conditions that can be used inside of a field description:

    Single Condition:

    export interface INodeFieldSingleCondition {
    	/** The key of the Field whose value should be matched */
    	key: string;
    	/** The expected value(s) that make this condition match */
    	value: TComparableValue | TComparableValue[];
    	/** If this is true, the condition result will be inverted */
    	negate?: boolean;
    	or?: never;
    	and?: never;
    }

    Example:

    {
        fields: [
            {
                [...field],
                condition: {
                    key: "displayField",
                    value: true
                }
            }
        ]
    }

    Only if the value of the other field with the key "displayField" is true, then this specific field should be displayed in the UI/Edit Node menu of the Flow Node.

    AND Condition:

    export interface INodeFieldANDCondition {
    	key?: never;
    	value?: never;
    	negate?: never;
    	or?: never;
    	/** Nested conditions are supported in version 4.1.4 and up of the Cognigy AI */
    	and: TNodeFieldCondition[]
    }

    Example:

    {
        fields: [
            {
                [...field],
                condition: {
                    and: [
                        {                
                            key: "displayField",
                            value: true
                        },
                        {                
                            key: "customerIsKnown",
                            value: true
                        },
                    ]
                }
            }
        ]
    }

    For a more detailed condition, the AND operator can be used. In this case, both conditions need to be true in order to display the field.

    OR Condition:

    export interface INodeFieldORCondition {
    	key?: never;
    	value?: never;
    	negate?: never;
    	/** Nested conditions are supported in version 4.1.4 and up of the Cognigy AI */
    	or: TNodeFieldCondition[]
    	and?: never;
    }

    Example:

    {
        fields: [
            {
                [...field],
                condition: {
                    or: [
                        {                
                            key: "displayField",
                            value: true
                        },
                        {                
                            key: "customerIsKnown",
                            value: true
                        },
                    ]
                }
            }
        ]
    }

    Finally, the OR operator tells Cognigy.AI to display the field if one of both keys is true -- in this example.

    function

    export type TNodeFunction = (params: INodeFunctionBaseParams) => Promise<void>;
    
    export interface INodeFunctionBaseParams {
    	cognigy: INodeExecutionCognigyObject;
    	childConfigs: TNodeChildConfigs[];
    	config: object;
    	nodeId: string;
    }
    childConfigs
    export type TNodeChildConfigs = {
    	id: string;
    	type: string;
    	config: {
    		[key: string]: unknown
    	};
    };

    preview

    export interface INodePreview {
    	type: "text" | "image" | "custom";
    	key: string;
    }

    tags

    type TCognigyNodeTagType = "basic" | "logic" | "message" | "profile" | "service" | "nlu" | "data";
    
    export type TNodeTagType = TCognigyNodeTagType | string;

    tokens

    export interface ISnippet {
    	/**
    	 * The label (name) of the snippet.
    	 */
    	label: string;
    
    	/**
    	 * The type of the snippet
    	 * based on what the snippet accesses
    	 * (profile, input or context)
    	 */
    	type: TSnippetType;
    
    	/**
    	 * The script the snippet executes
    	 */
    	script: string;
    }

    sections

    export interface INodeSection {
    	/** Unique identifier for this section within all sections of a descriptor, e.g. 'authentication' */
    	key: string;
    
    	/** Human readable lable of the seciton, e.g. 'Authentication' */
    	label: string;
    
    	/** Condition whether this section should be rendered */
    	condition?: TNodeFieldCondition;
    
    	/** Whether the sections should be collapsed by default (default: false) */
    	defaultCollapsed: boolean;
    
    	/** The fields that should be grouped in this section, points to 'key' of node-fields */
    	fields: string[];
    }

    form

    export interface INodeFieldAndSectionFormElement {
    	/** A key either pointing to a 'field -> key' or 'section -> key' */
    	key: string;
    
    	/** The type of the pointer, either 'field' or 'section' */
    	type: "field" | "section";
    }

    Options Resolvers

    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.

    Use Cases

    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).

    Usage Example

    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,
                };
              });
            },
          },
        },
      ],
    });

    API Reference

    Options Resolver

    The optionsResolver configuration can only be set on Node Fields with type select so far.

    Options Resolver Dependencies

    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 }).

    Resolver Function

    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.

    Dependencies & Triggers

    The resolverFunction has access to a config object containing a key-value-mapping of all registered dependency Fields (see section above).

    Everytime 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!).

    Resolved Options & Validation

    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.

    API & HTTP Requests

    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.

    Extension Localization

    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.

    The Localization Object

    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 Visibility

    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 Targets

    Localization doesn't work for all properties. Right now these properties are supported in a Descriptor:

    1. defaultLabel
    2. summary

    These properties are supported in a Node Section:

    1. label

    These properties are supported in a Node Field:

    1. label
    2. description

    If a Node Field is of type "select" then it's also supported in the label property of the options in the params

    Localization Example

    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
            }
        })

    Install

    npm i @cognigy/extension-tools

    DownloadsWeekly Downloads

    70

    Version

    0.14.0

    License

    SEE LICENSE IN LICENSE

    Unpacked Size

    52.5 kB

    Total Files

    16

    Last publish

    Collaborators

    • akirilyuk
    • monty0157
    • mayrbenjamin92
    • pedily
    • mastasky