Convert a JavaScript v3 bot to a skill

APPLIES TO: SDK v4

This article describes how to convert 2 sample JavaScript v3 bots to skills and to create a v4 skill consumer that can access these skills. To convert a .NET v3 bot to a skill, see how to Convert a .NET v3 bot to a skill. To migrate a JavaScript bot from v3 to v4, see how to Migrate a Javascript v3 bot to a v4 bot.

Prerequisites

  • Visual Studio Code.
  • Node.js.
  • An Azure subscription. If you don't have an Azure subscription, create a free account before you begin.
  • To test the skills, you will need the Bot Framework Emulator and local copies of the bots:

About the bots

In this article, each v3 bot is created to act as a skill. A v4 skill consumer is included, so that you can test the converted bots as skills.

  • The v3-skill-bot echoes back messages it receives. As a skill, it completes when it receives an "end" or "stop" message. The bot to convert is based on a minimal v3 bot.
  • The v3-booking-bot-skill allows the user to book a flight or a hotel. As a skill, it sends collected information back to the parent when finished.

Also, a v4 skill consumer, the v4-root-bot, demonstrates how to consume the skills and allows you to test them.

To use the skill consumer to test the skills, all 3 bots need to be running at the same time. The bots can be tested locally using the Bot Framework Emulator, with each bot using a different local port.

Create Azure resources for the bots

Bot-to-bot authentication requires that each participating bot has a valid app ID and password.

  1. Create a Bot Channels Registration for the bots as needed.
  2. Record the app ID and password for each one.

To convert a v3 bot

To convert an existing bot to a skill bot takes just a few steps, as outlined in the next couple sections. For more in-depth information, see about skills.

  • Update the bot's .env file to set the bot's app ID and password and to add a root bot app ID property.
  • Add claims validation. This will restrict access to the skill so that only users or your root bot can access the skill. See the additional information section for more information about default and custom claims validation.
  • Modify the bot's messages controller to handle endOfConversation activities from the root bot.
  • Modify the bot code to return an endOfConversation activity when the skill completes.
  • Whenever the skill completes, if it has conversation state or maintains resources, it should clear its conversation state and release resources.
  • Optionally add a manifest file. Since a skill consumer does not necessarily have access to the skill code, use a skill manifest to describe the activities the skill can receive and generate, its input and output parameters, and the skill's endpoints. The current manifest schema is skill-manifest-2.0.0.json.

Convert the echo bot

See Skills/v3-skill-bot for an example of a v3 echo bot that has been converted to a basic skill.

  1. Create a simple JavaScript v3 bot project and import required modules.

    v3-skill-bot/app.js

    const restify = require('restify');
    const builder = require('botbuilder');
    require('dotenv').config();
    
  2. Set the bot to run locally on port 3979.

    v3-skill-bot/app.js

    // Setup Restify Server
    const server = restify.createServer();
    server.listen(process.env.port || process.env.PORT || 3979, function () {
       console.log('%s listening to %s', server.name, server.url); 
    });
    
  3. In the configuration file, add the echo bot's app ID and password. Also, add a ROOT_BOT_APP_ID property with the simple root bot's app ID as its value.

    v3-skill-bot/.env

    # Bot Framework Credentials
    
    MICROSOFT_APP_ID=
    MICROSOFT_APP_PASSWORD=
    ROOT_BOT_APP_ID=
    
  4. Create the chat connector for the bot. This one uses the default authentication configuration. Set enableSkills to true to allow the bot to be used as a skill. allowedCallers is an array of the app IDs of the bots allowed to use this skill. If the first value of this array is '*', then any bot can use this skill.

    v3-skill-bot/app.js

    // Create chat connector for communicating with the Bot Framework Service
    const connector = new builder.ChatConnector({
        appId: process.env.MICROSOFT_APP_ID,
        appPassword: process.env.MICROSOFT_APP_PASSWORD,
        enableSkills: true,
        allowedCallers: [process.env.ROOT_BOT_APP_ID]
    });
    
  5. Update the message handler to send an endOfConversation activity when the user chooses to end the skill.

    v3-skill-bot/app.js

    // Create your bot with a function to receive messages from the user
    const bot = new builder.UniversalBot(connector, function (session) {
        switch (session.message.text.toLowerCase()) {
            case 'end':
            case 'stop':
                session.endConversation();
                break;
            default:
                session.send("Echo (JS V3) You said: %s", session.message.text);
                session.send('Say "end" or "stop" and I\'ll end the conversation and back to the parent.');
        }
    }).set('storage', inMemoryStorage); // Register in memory storage
    
  6. Since this bot does not maintain conversation state and does not create any resources for the conversation, the bot does not need to handle any endOfConversation activities that it receives from the skill consumer.

  7. Use this manifest for the echo bot. Set the endpoint app ID to the bot's app ID.

    v3-skill-bot/manifest/v3-skill-bot-manifest.json

    {
        "$schema": "https://schemas.botframework.com/schemas/skills/skill-manifest-2.0.0.json",
        "$id": "v3-skill-bot",
        "name": "Echo Skill bot",
        "version": "1.0",
        "description": "This is a sample echo skill",
        "publisherName": "Microsoft",
        "privacyUrl": "https://echoskillbot.contoso.com/privacy.html",
        "copyright": "Copyright (c) Microsoft Corporation. All rights reserved.",
        "license": "",
        "iconUrl": "https://echoskillbot.contoso.com/icon.png",
        "tags": [
          "sample",
          "echo"
        ],
        "endpoints": [
          {
            "name": "default",
            "protocol": "BotFrameworkV3",
            "description": "Default endpoint for the skill",
            "endpointUrl": "http://echoskillbot.contoso.com/api/messages",
            "msAppId": "00000000-0000-0000-0000-000000000000"
          }
        ]
      }
    

    For the skill-manifest schema, see skill-manifest-2.0.0.json.

Convert the booking bot

See Skills/v3-booking-bot-skill for an example of a v3 booking bot that has been converted to a basic skill. Before conversion, the bot was similar to the v3 core-MultiDialogs sample.

  1. Import required modules.

    v3-booking-bot-skill/app.js

    require('dotenv').config();
    
    var builder = require('botbuilder');
    var restify = require('restify');
    const skills = require('botbuilder/skills-validator');
    const { allowedCallersClaimsValidator } = require('./allowedCallersClaimsValidator');
    
  2. Set the bot to run locally on port 3980.

    v3-booking-bot-skill/app.js

    // Setup Restify Server
    var server = restify.createServer();
    server.listen(process.env.port || process.env.PORT || 3980, function () {
        console.log('%s listening to %s', server.name, server.url);
    });
    
  3. In the configuration file, add the booking bot's app ID and password. Also, add a ROOT_BOT_APP_ID property with the simple root bot's app ID as its value.

    v3-booking-bot-skill/.env

    # Bot Framework Credentials
    
    MICROSOFT_APP_ID=
    MICROSOFT_APP_PASSWORD=
    ROOT_BOT_APP_ID=
    
  4. Create the chat connector for the bot. This one uses a custom authentication configuration. Set enableSkills to true to allow the bot to be used as a skill. authConfiguration contains the custom authentication configuration object to use for authentication and claims validation.

    v3-booking-bot-skill/app.js

    // Create chat bot and listen to messages
    var connector = new builder.ChatConnector({
        appId: process.env.MICROSOFT_APP_ID,
        appPassword: process.env.MICROSOFT_APP_PASSWORD,
        enableSkills: true,
        authConfiguration: new skills.AuthenticationConfiguration([], allowedCallersClaimsValidator)
    });
    

    v3-booking-bot-skill/allowedCallersClaimsValidator.js

    This implements custom claims validation and throws an error if validation fails.

    const skills = require('botbuilder/skills-validator');
    const path = require('path');
    
    // Import required bot configuration.
    const ENV_FILE = path.join(__dirname, '.env');
    require('dotenv').config({ path: ENV_FILE });
    
    // Load the AppIds for the configured callers (we will only allow responses from skills we have configured).
    // process.env.AllowedCallers is the list of parent bot Ids that are allowed to access the skill
    // to add a new parent bot simply go to the .env file and add
    // the parent bot's Microsoft AppId to the list under AllowedCallers, e.g.:
    //  AllowedCallers=195bd793-4319-4a84-a800-386770c058b2,38c74e7a-3d01-4295-8e66-43dd358920f8
    const allowedCallers = [process.env.ROOT_BOT_APP_ID]; // process.env.AllowedCallers ? process.env.AllowedCallers.split(',') : undefined;
    
    /**
     * Sample claims validator that loads an allowed list from configuration if present
     * and checks that requests are coming from allowed parent bots.
     * @param claims An array of Claims decoded from the HTTP request's auth header
     */
    const allowedCallersClaimsValidator = async (claims) => {
        if (!allowedCallers || allowedCallers.length == 0) {
            throw new Error(`DefaultAuthenticationConfiguration allowedCallers must contain at least one element of '*' or valid MicrosoftAppId(s).`);
        }
        if (!claims || claims.length < 1) {
            throw new Error(`DefaultAuthenticationConfiguration.validateClaims.claims parameter must contain at least one element.`);
        }
        // If allowedCallers is undefined we allow all calls
        // If allowedCallers contains '*' we allow all callers
        if (skills.SkillValidation.isSkillClaim(claims)) {
                    
            if(allowedCallers[0] === '*') {
                return;
            }
            // Check that the appId claim in the skill request is in the list of skills configured for this bot.
            const appId = skills.JwtTokenValidation.getAppIdFromClaims(claims);
            if (allowedCallers.includes(appId)) {
                return;
            }
            throw new Error(`Received a request from a bot with an app ID of "${ appId }". To enable requests from this caller, add the app ID to your configuration file.`);
        }
        throw new Error(`DefaultAuthenticationConfiguration.validateClaims called without a Skill claim in claims.`);
    };
    
    module.exports.allowedCallersClaimsValidator = allowedCallersClaimsValidator;
    
  5. Update the message handler to send an endOfConversation activity when the skill ends. Note that session.endConversation() clears conversation state in addition to sending an endOfConversation activity.

    v3-booking-bot-skill/app.js

    Implement a helper function to set the endOfConversation activity's code and value properties and clear conversation state. If the bot managed any other resources for the conversation, you would release them here, too.

    // Code enum can be found here: https://aka.ms/codeEnum
    function endConversation(session, value = null, code = null) {
        session.send('Ending conversation from the skill...');
        // Send endOfConversation with custom code and values
        const msg = {
            value,
            code,
            type: 'endOfConversation'
        };
        session.send(msg);
        // Call endConversation() to clear state
        session.endConversation();
    }
    

    When the user completes the process, use the helper method to end the skill and return the user's collected data.

    var bot = new builder.UniversalBot(connector, [
        function (session) {
    
        },
        // Dialog has ended
        function(session, result) {
            endConversation(session, result, 'completedSuccessfully');
        }
    ]).set('storage', inMemoryStorage); // Register in memory storage
    

    If instead the user ends the process early, the helper method is still invoked.

    bot.recognizer({
        recognize: function (context, done) {
            var intent = { score: 0.0 };
            if (context.message.text) {
                switch (context.message.text.toLowerCase()) {
                    case 'help':
                        intent = { score: 1.0, intent: 'Help' };
                        break;
                    case 'end':
                        intent = { score: 1.0, intent: 'End' };
                        break;
                }
            }
            done(null, intent);
        }
    });
    
    // Add global endConversation() action bound to the 'Goodbye' intent
    bot.endConversationAction('endAction', "Ok... See you later.", { matches: 'End' });
    
  6. If the skill is cancelled by the skill consumer, the consumer sends an endOfConversation activity. Handle this activity and release any resources associated with the conversation.

    v3-booking-bot-skill/app.js

    // Listen for endOfConversation activities from other sources
    bot.on('endOfConversation', (message) => {
        bot.loadSession(message.address, (err, session) => {
            endConversation(session, null, 'completedSuccessfully');
        });
    })
    
  7. Use this manifest for the booking bot. Set the endpoint app ID to the bot's app ID.

    v3-booking-bot-skill/manifest/v3-booking-bot-skill-manifest.json

    {
        "$schema": "https://schemas.botframework.com/schemas/skills/skill-manifest-2.0.0.json",
        "$id": "v3-booking-bot-skill",
        "name": "Booking Skill bot",
        "version": "1.0",
        "description": "This is a sample booking skill",
        "publisherName": "Microsoft",
        "privacyUrl": "https://bookingskillbot.contoso.com/privacy.html",
        "copyright": "Copyright (c) Microsoft Corporation. All rights reserved.",
        "license": "",
        "iconUrl": "https://bookingskillbot.contoso.com/icon.png",
        "tags": [
          "sample",
          "echo"
        ],
        "endpoints": [
          {
            "name": "default",
            "protocol": "BotFrameworkV3",
            "description": "Default endpoint for the skill",
            "endpointUrl": "http://bookingskillbot.contoso.com/api/messages",
            "msAppId": "00000000-0000-0000-0000-000000000000"
          }
        ]
      }
    

    For the skill-manifest schema, see skill-manifest-2.0.0.json.

Create the v4 root bot

The simple root bot consumes the 2 skills and lets you verify that the conversion steps worked as planned. This bot will run locally on port 3978.

  1. To the configuration file, add the root bot's app ID and password. For each of the v3 skills, add the skill's app ID.

    v4-root-bot/.env

    MicrosoftAppId=
    MicrosoftAppPassword=
    SkillHostEndpoint=http://localhost:3978/api/skills
    
    SkillSimpleId=v3-skill-bot
    SkillSimpleAppId=
    SkillSimpleEndpoint=http://localhost:3979/api/messages
    
    SkillBookingId=v3-booking-bot-skill
    SkillBookingAppId=
    SkillBookingEndpoint=http://localhost:3980/api/messages
    

Test the root bot

Download and install the latest Bot Framework Emulator.

  1. Start all three bots locally on your machine.
  2. Use the Emulator to connect to the root bot.
  3. Test the skills and skill consumer.

Starting with version 4.11, you don't need an app ID and password to test a skill and skill consumer locally in the Emulator. An Azure subscription is still required to deploy your skill to Azure.

Additional information

Bot-to-bot authentication

The root and skill communicate over HTTP. The framework uses bearer tokens and bot application IDs to verify the identity of each bot. It uses an authentication configuration object to validate the authentication header on incoming requests. You must add a claims validator to the authentication configuration. The claims are evaluated after the authentication header. Your validation code should throw an error or exception to reject the request.

When creating a chat connector, include either an allowedCallers or an authConfiguration property in the settings parameter to enable bot-to-bot authentication.

The default claims validator for the chat connector uses the allowedCallers property. Its value should be an array of the application IDs of the bots that are allowed to call the skill. Set the first element to '*' to allow any bot to call the skill.

To use a custom claims validation function, set the authConfiguration field to your validation function. This function should accept an array of claim objects and throw an error if validation fails. Step 4 of the convert the booking bot section has an example claims validator.