Skip to content

Commit

Permalink
add skillDialog and associated classes (#1771)
Browse files Browse the repository at this point in the history
* add skillDialog and associated classes

* move non-impl Skill code to core for use in dialogs

* scaffold dialogRootBot and dialogSkillBot

* apply SkillDialog updates from microsoft/botbuilder-dotnet#3474

* rename SkillDialogArgs to BeginSkillDialogOptions

Co-authored-by: Steven Ickman <stevenic@microsoft.com>
  • Loading branch information
stevengum and Stevenic authored Mar 4, 2020
1 parent cc6d78e commit 69ae3f9
Show file tree
Hide file tree
Showing 24 changed files with 341 additions and 17 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
*.zip
*.vscode

# Functional Tests
libraries/functional-tests/**/lib
libraries/functional-tests/**/*.vscode/launch.json

# User-specific files
*.suo
*.user
Expand Down
2 changes: 2 additions & 0 deletions lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"libraries/botframework-connector",
"libraries/botframework-schema",
"libraries/functional-tests",
"libraries/functional-tests/dialogToDialog/dialogRootBot",
"libraries/functional-tests/dialogToDialog/dialogSkillBot",
"libraries/testbot",
"libraries/botframework-streaming",
"libraries/testskills/skillparent",
Expand Down
2 changes: 2 additions & 0 deletions libraries/botbuilder-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export * from './botTelemetryClient';
export * from './browserStorage';
export * from './cardFactory';
export * from './conversationState';
export * from './invokeResponse';
export * from './memoryStorage';
export * from './memoryTranscriptStore';
export * from './messageFactory';
Expand All @@ -26,6 +27,7 @@ export * from './privateConversationState';
export * from './propertyManager';
export * from './recognizerResult';
export * from './showTypingMiddleware';
export { BotFrameworkSkill, BotFrameworkClient, SkillConversationIdFactoryBase } from './skills';
export * from './skypeMentionNormalizeMiddleware';
export * from './storage';
export * from './telemetryLoggerMiddleware';
Expand Down
25 changes: 25 additions & 0 deletions libraries/botbuilder-core/src/skills/botFrameworkClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @module botbuilder
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { Activity } from 'botframework-schema';
import { InvokeResponse } from '../invokeResponse';

export abstract class BotFrameworkClient {
/**
* Forwards an activity to a another bot.
* @remarks
*
* @param fromBotId The MicrosoftAppId of the bot sending the activity.
* @param toBotId The MicrosoftAppId of the bot receiving the activity.
* @param toUrl The URL of the bot receiving the activity.
* @param serviceUrl The callback Url for the skill host.
* @param conversationId A conversation ID to use for the conversation with the skill.
* @param activity Activity to forward.
*/
public abstract postActivity(fromBotId: string, toBotId: string, toUrl: string, serviceUrl: string, conversationId: string, activity: Activity): Promise<InvokeResponse>
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @module botbuilder
* @module botbuilder-core
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
Expand Down
11 changes: 11 additions & 0 deletions libraries/botbuilder-core/src/skills/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* @module botbuilder-core
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

export { BotFrameworkClient } from './botFrameworkClient';
export { BotFrameworkSkill } from './botFrameworkSkill';
export { SkillConversationIdFactoryBase } from './skillConversationIdFactoryBase';
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Licensed under the MIT License.
*/

import { ConversationReference } from 'botbuilder-core';
import { ConversationReference } from 'botframework-schema';

/**
* Defines the methods of a factory that is used to create unique conversation IDs for skill conversations.
Expand Down
19 changes: 19 additions & 0 deletions libraries/botbuilder-dialogs/src/beginSkillDialogOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @module botbuilder-dialogs
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { Activity } from 'botbuilder-core';

/**
* A class with dialog arguments for a SkillDialog.
*/
export interface BeginSkillDialogOptions {
/**
* The Activity to send to the skill.
*/
activity: Activity;
}
3 changes: 3 additions & 0 deletions libraries/botbuilder-dialogs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ export * from './dialogContainer';
export * from './dialogContext';
export * from './dialogEvents';
export * from './dialogSet';
export * from './skillDialog';
export * from './skillDialogOptions';
export * from './beginSkillDialogOptions';
export * from './waterfallDialog';
export * from './waterfallStepContext';
140 changes: 140 additions & 0 deletions libraries/botbuilder-dialogs/src/skillDialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* @module botbuilder-dialogs
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import {
Activity,
ActivityTypes,
ConversationReference,
TurnContext
} from 'botbuilder-core';
import {
Dialog,
DialogInstance,
DialogReason,
DialogTurnResult
} from './dialog';
import { DialogContext } from './dialogContext';
import { BeginSkillDialogOptions } from './beginSkillDialogOptions';
import { SkillDialogOptions } from './skillDialogOptions';

export class SkillDialog extends Dialog {
protected dialogOptions: SkillDialogOptions;

/**
* A sample dialog that can wrap remote calls to a skill.
*
* @remarks
* The options parameter in `beginDialog()` must be a `SkillDialogArgs` object with the initial parameters
* for the dialog.
*
* @param dialogOptions
* @param dialogId
*/
public constructor(dialogOptions: SkillDialogOptions, dialogId?: string) {
super(dialogId);
if (!dialogOptions) {
throw new TypeError('Missing dialogOptions parameter');
}
this.dialogOptions = dialogOptions;
}

public async beginDialog(dc: DialogContext, options?: {}): Promise<DialogTurnResult> {
const dialogArgs = SkillDialog.validateBeginDialogArgs(options);

await dc.context.sendTraceActivity(`${ this.id }.beginDialog()`, undefined, undefined, `Using activity of type: ${ dialogArgs.activity.type }`);

// Create deep clone of the original activity to avoid altering it before forwarding it.
const clonedActivity = this.cloneActivity(dialogArgs.activity);

// Apply conversation reference and common properties from incoming activity before sending.
const skillActivity = TurnContext.applyConversationReference(clonedActivity, TurnContext.getConversationReference(dc.context.activity), true) as Activity;

// Send the activity to the skill.
await this.sendToSkill(dc.context, skillActivity);
return Dialog.EndOfTurn;
}

public async continueDialog(dc: DialogContext): Promise<DialogTurnResult> {
await dc.context.sendTraceActivity(`${ this.id }.continueDialog()`, undefined, undefined, `ActivityType: ${ dc.context.activity.type }`);


// Handle EndOfConversation from the skill (this will be sent to the this dialog by the SkillHandler if received from the Skill)
if (dc.context.activity.type === ActivityTypes.EndOfConversation) {
await dc.context.sendTraceActivity(`${ this.id }.continueDialog()`, undefined, undefined, `Got ${ ActivityTypes.EndOfConversation }`);
return await dc.endDialog(dc.context.activity.value);
}

// Forward only Message and Event activities to the skill
if (dc.context.activity.type === ActivityTypes.Message || dc.context.activity.type === ActivityTypes.Event) {
// Just forward to the remote skill
await this.sendToSkill(dc.context, dc.context.activity);
}

return Dialog.EndOfTurn;
}

public async endDialog(context: TurnContext, instance: DialogInstance, reason: DialogReason): Promise<void> {
// Send of of conversation to the skill if the dialog has been cancelled.
if (reason == DialogReason.cancelCalled || reason == DialogReason.replaceCalled) {
await context.sendTraceActivity(`${ this.id }.EndDialogAsync()`, undefined, undefined, `ActivityType: ${ context.activity.type }`);

const reference = TurnContext.getConversationReference(context.activity);
// Apply conversation reference and common properties from incoming activity before sending.
const activity = TurnContext.applyConversationReference({ type: ActivityTypes.EndOfConversation }, reference, true);
activity.channelData = context.activity.channelData;

await this.sendToSkill(context, activity as Activity);
}

await super.endDialog(context, instance, reason);
}

/**
* Clones the Activity entity.
* @param activity Activity to clone.
*/
private cloneActivity(activity: Partial<Activity>): Activity {
return Object.assign({} as Activity, activity);
}

private static validateBeginDialogArgs(options: any): BeginSkillDialogOptions {
if (!options) {
throw new TypeError('Missing options parameter');
}

const dialogArgs = options as BeginSkillDialogOptions;

if (!dialogArgs.activity) {
throw new TypeError(`"activity" is undefined or null in options.`);
}

// Only accept Message or Event activities
if (dialogArgs.activity.type !== ActivityTypes.Message && dialogArgs.activity.type !== ActivityTypes.Event) {
// Just forward to the remote skill
throw new TypeError(`Only ${ ActivityTypes.Message } and ${ ActivityTypes.Event } activities are supported. Received activity of type ${ dialogArgs.activity.type } in options.`);
}

return dialogArgs;
}

private async sendToSkill(context: TurnContext, activity: Activity): Promise<void> {
// Create a conversationId to interact with the skill and send the activity
const skillConversationId = await this.dialogOptions.conversationIdFactory.createSkillConversationId(TurnContext.getConversationReference(activity) as ConversationReference);

// Always save state before forwarding
// (the dialog stack won't get updated with the skillDialog and things won't work if you don't)
await this.dialogOptions.conversationState.saveChanges(context, true);
const skillInfo = this.dialogOptions.skill;
const response = await this.dialogOptions.skillClient.postActivity(this.dialogOptions.botId, skillInfo.appId, skillInfo.skillEndpoint, skillInfo.skillEndpoint, skillConversationId, activity);

// Inspect the skill response status
if (!(response.status >= 200 && response.status <= 299)) {
throw new Error(`Error invoking the skill id: "${ skillInfo.id }" at "${ skillInfo.skillEndpoint }" (status is ${ response.status }). \r\n ${ response.body }`);
}
}
}
46 changes: 46 additions & 0 deletions libraries/botbuilder-dialogs/src/skillDialogOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* @module botbuilder-dialogs
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import {
BotFrameworkClient,
BotFrameworkSkill,
ConversationState,
SkillConversationIdFactoryBase
} from 'botbuilder-core';

export interface SkillDialogOptions {
/**
* The the Microsoft app ID of the bot calling the skill.
*/
botId: string;

/**
* The BotFrameworkSkill that the dialog will call.
*/
conversationIdFactory: SkillConversationIdFactoryBase;

/**
* The ConversationState to be used by the Dialog.
*/
conversationState: ConversationState;

/**
* The BotFrameworkSkill the dialog will call.
*/
skill: BotFrameworkSkill;

/**
* The BotFrameworkClient used to call the remote skill.
*/
skillClient: BotFrameworkClient;

/**
* The callback Url for the skill host.
*/
skillHostEndpoint: string;
}
4 changes: 2 additions & 2 deletions libraries/botbuilder/src/botFrameworkAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@

import { STATUS_CODES } from 'http';
import * as os from 'os';
import { Activity, ActivityTypes, BotAdapter, BotCallbackHandlerKey, ChannelAccount, ConversationAccount, ConversationParameters, ConversationReference, ConversationsResult, DeliveryModes, CredentialTokenProvider, ResourceResponse, TokenResponse, TurnContext } from 'botbuilder-core';
import { Activity, ActivityTypes, BotAdapter, BotCallbackHandlerKey, ChannelAccount, ConversationAccount, ConversationParameters, ConversationReference, ConversationsResult, DeliveryModes, CredentialTokenProvider, InvokeResponse, ResourceResponse, TokenResponse, TurnContext } from 'botbuilder-core';
import { AuthenticationConfiguration, AuthenticationConstants, ChannelValidation, ClaimsIdentity, ConnectorClient, EmulatorApiClient, GovernmentConstants, GovernmentChannelValidation, JwtTokenValidation, MicrosoftAppCredentials, AppCredentials, CertificateAppCredentials, SimpleCredentialProvider, TokenApiClient, TokenStatus, TokenApiModels, SkillValidation } from 'botframework-connector';
import { INodeBuffer, INodeSocket, IReceiveRequest, ISocket, IStreamingTransportServer, NamedPipeServer, NodeWebSocketFactory, NodeWebSocketFactoryBase, RequestHandler, StreamingResponse, WebSocketServer } from 'botframework-streaming';

import { InvokeResponse, WebRequest, WebResponse } from './interfaces';
import { WebRequest, WebResponse } from './interfaces';
import { defaultPipeName, GET, POST, MESSAGES_PATH, StreamingHttpClient, TokenResolver, VERSION_PATH } from './streaming';

export enum StatusCodes {
Expand Down
7 changes: 4 additions & 3 deletions libraries/botbuilder/src/botFrameworkHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import axios from 'axios';
import { Activity } from 'botbuilder-core';
import { Activity, BotFrameworkClient, InvokeResponse } from 'botbuilder-core';
import {
AuthenticationConstants,
GovernmentConstants,
Expand All @@ -17,19 +17,20 @@ import {
} from 'botframework-connector';

import { USER_AGENT } from './botFrameworkAdapter';
import { InvokeResponse } from './interfaces';

/**
* HttpClient for calling skills from a Node.js BotBuilder V4 SDK bot.
*/
export class BotFrameworkHttpClient {
export class BotFrameworkHttpClient extends BotFrameworkClient {
/**
* Cache for appCredentials to speed up token acquisition (a token is not requested unless is expired)
* AppCredentials are cached using appId + scope (this last parameter is only used if the app credentials are used to call a skill)
*/
private static readonly appCredentialMapCache: Map<string, MicrosoftAppCredentials> = new Map<string, MicrosoftAppCredentials>();


public constructor(private readonly credentialProvider: ICredentialProvider, private readonly channelService?: string) {
super();
if (!this.credentialProvider) {
throw new Error('BotFrameworkHttpClient(): missing credentialProvider');
}
Expand Down
1 change: 0 additions & 1 deletion libraries/botbuilder/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export { ChannelServiceRoutes, RouteHandler, WebServer } from './channelServiceR
export * from './fileTranscriptStore';
export * from './inspectionMiddleware';
export {
InvokeResponse,
WebRequest,
WebResponse
} from './interfaces';
Expand Down
1 change: 0 additions & 1 deletion libraries/botbuilder/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@
* Licensed under the MIT License.
*/

export * from './invokeResponse';
export * from './webRequest';
export * from './webResponse';
2 changes: 0 additions & 2 deletions libraries/botbuilder/src/skills/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,5 @@
* Licensed under the MIT License.
*/

export * from './botFrameworkSkill';
export * from './skillConversationIdFactoryBase';
export * from './skillHandler';
export * from './skillHttpClient';
2 changes: 1 addition & 1 deletion libraries/botbuilder/src/skills/skillHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import {
ActivityTypes,
BotAdapter,
ResourceResponse,
SkillConversationIdFactoryBase,
TurnContext
} from 'botbuilder-core';
import { AuthenticationConfiguration, AppCredentials, ICredentialProvider, ClaimsIdentity } from 'botframework-connector';

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

/**
Expand Down
Loading

0 comments on commit 69ae3f9

Please sign in to comment.