Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont we have constants for this? Or otherwise it might be worth checking if in other places we do something like this which owuld be 'isSuccessStatusCode'. C# has a built in method for this actually, given how frequently folks ask this question. Potentially something to think about in general rather than for this PR, definitely would not block on it.

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