Converting a Node Agent SDK Project to Messaging Platform SDK

The Node Agent SDK (NASDK) is deprecated, and transitioning to the Messaging Platform SDK (MPSDK) is recommended. There are two types of conversions:

  1. Replacement Conversion
  2. Rework Conversion

Replacement Conversion

In a replacement conversion, NASDK applications that subscribe to conversation and ring events, issue requests, and consume responses are adapted to MPSDK. The MPSDK exposes responses and events as a stream of JSON objects, similar to NASDK, without requiring the use of abstractions like conversations or dialogs. The converted application benefits from the reconnection logic and automated token maintenance of MPSDK. However, ensure that your existing code, built to work around NASDK’s shortcomings, is compatible with the new MPSDK implementation.

Rework Conversion

In a rework conversion, the application leverages the abstractions offered by MPSDK. MPSDK consumes the stream of JSON responses and events and builds up stateful abstractions for you. This conversion may require significant changes to your application, shifting from working with streams to handling state changes. For instance, instead of consuming a notification to determine that a participant was added, the MPSDK will emit a participant-added event.

General Recommendation: If you have a running NASDK application, start with a replacement conversion to avoid complications where custom code has no direct translation to MPSDK abstractions. After this initial conversion, consider a full rework if necessary.


Replacement vs. Rework Example

Consider the following NASDK code, which sends hello world! upon the agent joining a conversation:

const Agent = require('AgentSDK');

// Connects right away
const connection = new Agent({
    'accountId': 'testAccount',
    'username': 'test',
    'appKey': 'test',
    'secret': 'test',
    'accessToken': 'test',
    'accessTokenSecret': 'test'
});

const agentId = 'testAccount.testBot';

connection.on('connected', () => {
    // The bot is ready to receive rings
    connection.setAgentState({availability: 'ONLINE'});
    // Subscribe to all open conversations for this agent
    connection.subscribeExConversations({
        'agentIds': [agentId],
        'convState': ['OPEN']
    }, (e, resp) => console.log('subscribeExConversations', this.conf.id || '', resp || e));

    // The bot should be ringed for conversations.
    connection.subscribeRoutingTasks({});
    // Start the keep-alive process by piggybacking the getClock message
    connection._pingClock = setInterval(connection.getClock, 30000);
});

connection.on('routing.RoutingTaskNotification', ring => {
    // Accept any ring/routing task.
    // The bot will join the conversation
    ring.changes.forEach(c => {
        if (c.type === 'UPSERT') {
            c.result.ringsDetails.forEach(r => {
                if (r.ringState === 'WAITING') {
                    connection.updateRingState({
                        'ringId': r.ringId,
                        'ringState': 'ACCEPTED'
                    }, (e, resp) => console.log(resp));
                }
            });
        }
    });
});

connection.on('cqm.ExConversationChangeNotification', notification => {
    // Extract conversation id from the first change.
    // The assumption is that the notification carries at least one change.
    // This will break if the assumption is not true.
    const conversationId = notification.change[0].result.convId;
    connection.publishEvent({
        dialogId: conversationId,
        event: {
            type: 'ContentEvent',
            contentType: 'text/plain',
            message: 'hello world!'
        }
    });
});

This NASDK code processes events using callbacks defined on the connection. NASDK doesn’t maintain internal state, leaving state management to the developer. NASDK only supports authenticated agent connections.


Replacement Conversion Example

In the replacement conversion, requests are structured differently, using the body key for requests and the type key to specify the request type. The stream is consumed with callbacks on the connection, but event names differ slightly. Configuring and opening a connection are now separate steps, and the async/await style is used.

const authData = {
    "username": "botUser",
    "appKey": "6828c2SomeKey",
    "secret": "49c59aSomeSecret",
    "accessToken": "efde28SomeToken",
    "accessTokenSecret": "6f800SomeTokenSecret"
};

// Create a connection with a default subscription to all
// open conversations where the this agent is part of.
const connection = lpm.createConnection({
    appId: `example_brand_connection`,
    accountId: '12345678',
    userType: lpm.UserType.BRAND,
    authData,
    // Determine that the connection is interested in conversations
    // matching the following criteria by default.
    defaultSubscriptionQuery: {
        'agentId': ['12345678.agent'], // Important: agentId instead of agentIds
        'state': ['OPEN'] // Important: state instead of convState
    }
});

connection.on('.ams.routing.RoutingTaskNotification', ring => {
    // Accept any ring/routing task.
    // The bot will join the conversation.
    ring.changes.forEach(c => {
        if (c.type === 'UPSERT') {
            c.result.ringsDetails.forEach(r => {
                if (r.ringState === 'WAITING') {
                    connection._updateRingState({
                        'ringId': r.ringId,
                        'ringState': 'ACCEPTED'
                    }, (e, resp) => console.log(resp));
                }
            });
        }
    });
});

connection.on('.ams.aam.ExConversationChangeNotification', notification => {
    // Extract conversation id from the first change.
    // The assumption is that the notification carries at least one change.
    // This will break if the assumption is not true.
    const conversationId = notification.change[0].result.convId;

    connection.send({
        type: '.ams.ms.PublishEvent',
        body: {
            dialogId: conversationId,
            event: {
                type: 'ContentEvent',
                contentType: 'text/plain',
                message: 'hello world!'
            }
        }
    });
});

// Different to the NASDK, the connection has to be opened explicitly
await connection.open();
// Register to be able to accept rings
await connection.createRoutingTaskSubscription();
// The bot is ready to receive rings
await connection.setAgentState({ agentState: lpm.AgentState.ONLINE });

Rework Conversion Example

Using MPSDK abstractions, your code will primarily react to state changes rather than consuming raw events directly:

const authData = {
    "username": "botUser",
    "appKey": "6828c2SomeKey",
    "secret": "49c59aSomeSecret",
    "accessToken": "efde28SomeToken",
    "accessTokenSecret": "6f800SomeTokenSecret"
};

// Create a connection with a default subscription to all
// open conversations where the this agent is part of.
const connection = lpm.createConnection({
    appId: `example_brand_connection`,
    accountId: '12345678',
    userType: lpm.UserType.BRAND,
    authData,
    // Determine that the connection is interested in conversations
    // matching the following criteria by default.
    defaultSubscriptionQuery: {
        'agentId': ['12345678.agentId'], // Important: agentId instead of agentIds
        'state': ['OPEN'] // Important: state instead of convState
    }
});

connection.on('conversation', async conversation => {
    await conversation.sendMessage('hello world');
});

connection.on('ring', async ring => {
    // Only accept rings which are waiting
    if (ring.ringState !== RingState.WAITING) return;
    await ring.accept();
});

// Open the connection
await connection.open();
// Register to be able to accept rings
await connection.createRoutingTaskSubscription();
// The bot is ready to receive rings
await connection.setAgentState({ agentState: lpm.AgentState.ONLINE });

For more complex interactions, the ring object offers a convenience method ring.conversation() allowing you to await the conversation:

connection.on('ring', async ring => {
    if (ring.ringState !== RingState.WAITING) return;
    await ring.accept();
    // Awaiting the conversation would pause any subsequent code. Instead, you can utilize then if there is code which 
    // should be run without a conversation.
    // Per default waits for two seconds for the conversation to arrive.
    ring.conversation().then(async (conversation) => {
        await conversation.sendMessage('hello world');
    });
    
    // Ring received will be printed right away
    console.log('Ring received');
});

This approach makes the code more concise but requires a complete project conversion.


All Request Types

Converting NASDK requests to MPSDK requests involves wrapping partial requests in an object containing the request type:

const partialPublishEventRequest = {
    dialogId: 'MY_DIALOG_ID',
    event: {
        type: 'ContentEvent',
        contentType: 'text/plain',
        message: 'hello world!'
    }
};

const requestType = '.ams.ms.PublishEvent';

const fullPublishEventRequest = {
    type: requestType,
    body: partialPublishEventRequest
};

const response = await connection.send(fullPublishEventRequest);

Other partial requests can be converted similarly. Note that certain requests will need additional tweaking before they are sent. Use the following table to look up request types and the next section for a list of required changes per request type:

const REQUEST_TYPES = {
    getClock:                      '.GetClock',
    agentRequestConversation:      '.ams.cm.AgentRequestConversation',
    subscribeExConversations:      '.ams.aam.SubscribeExConversations',
    unsubscribeExConversations:    '.ams.aam.UnsubscribeExConversations',
    updateConversationField:       '.ams.cm.UpdateConversationField',
    publishEvent:                  '.ams.ms.PublishEvent',
    updateRingState:               '.ams.routing.UpdateRingState',
    subscribeRoutingTasks:         '.ams.routing.SubscribeRoutingTasks',
    updateRoutingTaskSubscription: '.ams.routing.UpdateRoutingTaskSubscription',
    getUserProfile:                '.ams.userprofile.GetUserProfile',
    setAgentState:                 '.ams.routing.SetAgentState',
    subscribeAgentsState:          '.ams.routing.SubscribeAgentsState',
    subscribeMessagingEvents:      'ms.SubscribeMessagingEvents',
    generateURLForDownloadFile:    '.ams.ms.GenerateURLForDownloadFile',
    generateURLForUploadFile:      '.ams.ms.GenerateURLForUploadFile',
    generateDownloadToken:         '.ams.ms.token.GenerateDownloadToken'
};

const fullRequest = {
    type: REQUEST_TYPES['publishEvent'], // Look up the type for publish event
    body: existingJsonRequest
}

const result = await connection.send(fullRequest);

Required changes to the request

Request type Changes
.ams.cm.AgentRequestConversation body.channelType should be equal to 'MESSAGING'
.ams.cm.UpdateConversationField Every member of body.conversationField whose property is called ParticipantsChange should have a property userId equal to the agentId
.ams.routing.SubscribeRoutingTasks body.channelType should be equal to 'MESSAGING'
body.agentId should be equal to the agent id
body.brandId should be equal to the accountId
.ams.routing.SetAgentState body.channels should be an array with a value of ['MESSAGING']
body.agentUserId should be equal to the agent id
.ams.routing.SubscribeAgentsState body.agentId should be equal to the agent id
body.brandId should be equal to the accountId
ms.SubscribeMessagingEvents type should be equal to .ams.ms.QueryMessages
body.newerThanSequence should contain either a sequence number or 0