Skip to content

[Skills] add SkillHandler & SkillConversationIdFactoryBase classes #1482

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Dec 9, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libraries/botbuilder/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export { ChannelServiceHandler } from './channelServiceHandler';
export { ChannelServiceRoutes } from './channelServiceRoutes';
export * from './fileTranscriptStore';
export * from './inspectionMiddleware';
export * from './skills';
export * from './streaming';
export * from './teamsActivityHandler';
export * from './teamsActivityHelpers';
Expand Down
10 changes: 10 additions & 0 deletions libraries/botbuilder/src/skills/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* @module botbuilder
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

export * from './skillConversationIdFactoryBase';
export * from './skillHandler';
36 changes: 36 additions & 0 deletions libraries/botbuilder/src/skills/skillConversationIdFactoryBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* @module botbuilder
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { ConversationReference } from 'botbuilder-core';

/**
* Defines the methods of a factory that is used to create unique conversation IDs for skill conversations.
*/
export abstract class SkillConversationIdFactoryBase {
/**
* Creates a conversation ID for a skill conversation based on the caller's ConversationReference.
* @remarks
* It should be possible to use the returned string on a request URL and it should not contain special characters.
* @param conversationReference The skill's caller ConversationReference.
* @returns A unique conversation ID used to communicate with the skill.
*/
public abstract createSkillConversationId(conversationReference: ConversationReference): Promise<string>;

/**
* Gets the ConversationReference created using createSkillConversationId() for a skillConversationId.
* @param skillConversationId >A skill conversationId created using createSkillConversationId().
* @returns The caller's ConversationReference for a skillConversationId. null if not found.
*/
public abstract getConversationReference(skillConversationId: string): Promise<ConversationReference>;

/**
* Deletes a ConversationReference.
* @param skillConversationId A skill conversationId created using createSkillConversationId().
*/
public abstract deleteConversationReference(skillConversationId: string): Promise<void>;
}
181 changes: 181 additions & 0 deletions libraries/botbuilder/src/skills/skillHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* @module botbuilder
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import {
Activity,
ActivityHandlerBase,
ActivityTypes,
BotAdapter,
ResourceResponse,
TurnContext
} from 'botbuilder-core';
import { AuthenticationConfiguration, ICredentialProvider, ClaimsIdentity } from 'botframework-connector';

import { ChannelServiceHandler } from '../channelServiceHandler';
import { SkillConversationIdFactoryBase } from './skillConversationIdFactoryBase';
import { BotFrameworkAdapter } from '../botFrameworkAdapter';

/**
* A Bot Framework Handler for skills.
*/
export class SkillHandler extends ChannelServiceHandler {
/**
* Initializes a new instance of the SkillHandler class.
* @param adapter An instance of the BotAdapter that will handle the request.
* @param bot The ActivityHandlerBase instance.
* @param conversationIdFactory A SkillConversationIdFactoryBase to unpack the conversation ID and map it to the calling bot.
* @param credentialProvider The credential provider.
* @param authConfig The authentication configuration.
* @param channelService The string indicating if the bot is working in Public Azure or in Azure Government (https://aka.ms/AzureGovDocs).
*/
constructor(
private readonly adapter: BotAdapter,
private readonly bot: ActivityHandlerBase,
private readonly conversationIdFactory: SkillConversationIdFactoryBase,
credentialProvider: ICredentialProvider,
authConfig: AuthenticationConfiguration,
channelService?: string
) {
super(credentialProvider, authConfig, channelService);
if (!adapter) {
throw new Error('missing adapter.');
}
if (!conversationIdFactory) {
throw new Error('missing conversationIdFactory.');
}
}

/**
* sendToConversation() API for Skill.
* @remarks
* This method allows you to send an activity to the end of a conversation.
*
* This is slightly different from replyToActivity().
* * sendToConversation(conversationId) - will append the activity to the end
* of the conversation according to the timestamp or semantics of the channel.
* * replyToActivity(conversationId,ActivityId) - adds the activity as a reply
* to another activity, if the channel supports it. If the channel does not
* support nested replies, replyToActivity falls back to sendToConversation.
*
* Use replyToActivity when replying to a specific activity in the conversation.
*
* Use sendToConversation in all other cases.
* @param claimsIdentity ClaimsIdentity for the bot, should have AudienceClaim, AppIdClaim and ServiceUrlClaim.
* @param conversationId Conversation ID.
* @param activity Activity to send.
* @returns A Promise with a ResourceResponse.
*/
protected async onSendToConversation(claimsIdentity: ClaimsIdentity, conversationId: string, activity: Activity): Promise<ResourceResponse> {
return await this.processActivity(claimsIdentity, conversationId, null, activity);
}

/**
* replyToActivity() API for Skill.
* @remarks
* This method allows you to reply to an activity.
*
* This is slightly different from sendToConversation().
* * sendToConversation(conversationId) - will append the activity to the end
* of the conversation according to the timestamp or semantics of the channel.
* * replyToActivity(conversationId,ActivityId) - adds the activity as a reply
* to another activity, if the channel supports it. If the channel does not
* support nested replies, replyToActivity falls back to sendToConversation.
*
* Use replyToActivity when replying to a specific activity in the conversation.
*
* Use sendToConversation in all other cases.
* @param claimsIdentity ClaimsIdentity for the bot, should have AudienceClaim, AppIdClaim and ServiceUrlClaim.
* @param conversationId Conversation ID.
* @param activityId activityId the reply is to.
* @param activity Activity to send.
* @returns A Promise with a ResourceResponse.
*/
protected async onreplyToActivity(claimsIdentity: ClaimsIdentity, conversationId: string, activityId: string, activity: Activity): Promise<ResourceResponse> {
return await this.processActivity(claimsIdentity, conversationId, activityId, activity);
}

private static applyEoCToTurnContextActivity(turnContext: TurnContext,endOfConversationActivity: Activity): void {
// transform the turnContext.activity to be an EndOfConversation Activity.
turnContext.activity.type = endOfConversationActivity.type;
turnContext.activity.text = endOfConversationActivity.text;
turnContext.activity.code = endOfConversationActivity.code;

turnContext.activity.replyToId = endOfConversationActivity.replyToId;
turnContext.activity.value = endOfConversationActivity.value;
turnContext.activity.entities = endOfConversationActivity.entities;
turnContext.activity.localTimestamp = endOfConversationActivity.localTimestamp;
turnContext.activity.timestamp = endOfConversationActivity.timestamp;
turnContext.activity.channelData = endOfConversationActivity.channelData;
}

private static applyEventToTurnContextActivity(turnContext: TurnContext, eventActivity: Activity): void {
// transform the turnContext.activity to be an Event Activity.
turnContext.activity.type = eventActivity.type;
turnContext.activity.name = eventActivity.name;
turnContext.activity.value = eventActivity.value;
turnContext.activity.relatesTo = eventActivity.relatesTo;

turnContext.activity.replyToId = eventActivity.replyToId;
turnContext.activity.value = eventActivity.value;
turnContext.activity.entities = eventActivity.entities;
turnContext.activity.localTimestamp = eventActivity.localTimestamp;
turnContext.activity.timestamp = eventActivity.timestamp;
turnContext.activity.channelData = eventActivity.channelData;
}

private async processActivity(claimsIdentity: ClaimsIdentity, conversationId: string, replyToActivityId: string, activity: Activity): Promise<ResourceResponse> {
const conversationReference = await this.conversationIdFactory.getConversationReference(conversationId);

if (!conversationReference) {
throw new Error('conversationReference not found.');
}

/**
* Callback passed to the BotFrameworkAdapter.createConversation() call.
* This function does the following:
* - Caches the ClaimsIdentity on the TurnContext.turnState
* - Applies the correct ConversationReference to the Activity for sending to the user-router conversation.
* - For EndOfConversation Activities received from the Skill, removes the ConversationReference from the
* ConversationIdFactory
*/
const callback = async (context: TurnContext): Promise<void> => {
// Cache the ClaimsIdentity on the context so that it's available inside of the bot's logic.
context.turnState.set((context.adapter as BotFrameworkAdapter).BotIdentityKey, claimsIdentity);

TurnContext.applyConversationReference(activity, conversationReference);
context.activity.id = replyToActivityId;
switch (activity.type)
{
case ActivityTypes.EndOfConversation:
await this.conversationIdFactory.deleteConversationReference(conversationId);
SkillHandler.applyEoCToTurnContextActivity(context, activity);
await this.bot.run(context);
break;
case ActivityTypes.Event:
SkillHandler.applyEventToTurnContextActivity(context, activity);
await this.bot.run(context);
break;
default:
await context.sendActivity(activity);
break;
}
};

await this.adapter.continueConversation(conversationReference, callback);
return { id: uuid() };
}
}

// Helper function to generate an UUID.
// Code is from @stevenic: https://github.com/stevenic
function uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}