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
1 change: 1 addition & 0 deletions libraries/botbuilder-core/src/botAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export abstract class BotAdapter {
protected middleware: MiddlewareSet = new MiddlewareSet();
private turnError: (context: TurnContext, error: Error) => Promise<void>;
public readonly BotIdentityKey: Symbol = Symbol('BotIdentity');
public readonly OAuthScopeKey: Symbol = Symbol('OAuthScope');

/**
* Asynchronously sends a set of outgoing activities to a channel server.
Expand Down
3 changes: 2 additions & 1 deletion libraries/botbuilder-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export * from './privateConversationState';
export * from './propertyManager';
export * from './recognizerResult';
export * from './showTypingMiddleware';
export { BotFrameworkSkill, BotFrameworkClient, SkillConversationIdFactoryBase } from './skills';
export { BotFrameworkSkill, BotFrameworkClient, SkillConversationIdFactoryBase, SkillConversationReference,
SkillConversationIdFactoryOptions } from './skills';
export * from './skypeMentionNormalizeMiddleware';
export * from './storage';
export * from './telemetryLoggerMiddleware';
Expand Down
4 changes: 2 additions & 2 deletions libraries/botbuilder-core/src/invokeResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*
* This interface supports the framework and is not intended to be called directly for your code.
*/
export interface InvokeResponse {
export interface InvokeResponse<T = any> {
/**
* The HTTP status code of the response.
*/
Expand All @@ -20,5 +20,5 @@ export interface InvokeResponse {
/**
* Optional. The body of the response.
*/
body?: any;
body?: T;
}
2 changes: 1 addition & 1 deletion libraries/botbuilder-core/src/skills/botFrameworkClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ export abstract class BotFrameworkClient {
* @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>
public abstract postActivity<T>(fromBotId: string, toBotId: string, toUrl: string, serviceUrl: string, conversationId: string, activity: Activity): Promise<InvokeResponse<T>>
}
2 changes: 2 additions & 0 deletions libraries/botbuilder-core/src/skills/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
export { BotFrameworkClient } from './botFrameworkClient';
export { BotFrameworkSkill } from './botFrameworkSkill';
export { SkillConversationIdFactoryBase } from './skillConversationIdFactoryBase';
export { SkillConversationReference } from './skillConversationReference';
export { SkillConversationIdFactoryOptions } from './skillConversationIdFactoryOptions';
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,54 @@
*/

import { ConversationReference } from 'botframework-schema';
import { SkillConversationIdFactoryOptions } from './skillConversationIdFactoryOptions';
import { SkillConversationReference } from './skillConversationReference';

/**
* Defines the methods of a factory that is used to create unique conversation IDs for skill conversations.
*/
export abstract class SkillConversationIdFactoryBase {
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>;
public createSkillConversationIdWithOptions(options: SkillConversationIdFactoryOptions): Promise<string> {
throw new Error('Not Implemented');
}


/**
* Creates a conversation ID for a skill conversation based on the caller's ConversationReference.
* @deprecated Method is deprecated, please use createSkillConversationIdWithOptions() with SkillConversationIdFactoryOptions instead.
* @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 createSkillConversationId(conversationReference: ConversationReference): Promise<string> {
throw new Error('Not Implemented');
}

/**
* Gets the ConversationReference created using createSkillConversationId() for a skillConversationId.
* @deprecated Method is deprecated, please use getSkillConversationReference() instead.
* @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>;
public getConversationReference(skillConversationId: string): Promise<ConversationReference> {
throw new Error('Not Implemented');
}

/**
* Gets the SkillConversationReference created using createSkillConversationId() for a skillConversationId.
* @param skillConversationId Gets the SkillConversationReference used during CreateSkillConversationIdAsync for a skillConversationId.
*/
public getSkillConversationReference(skillConversationId: string): Promise<SkillConversationReference> {
throw new Error('Not Implemented');
}

/**
* Deletes a ConversationReference.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* @module botbuilder-core
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { Activity } from 'botframework-schema';
import { BotFrameworkSkill } from './botFrameworkSkill';

export interface SkillConversationIdFactoryOptions {
/**
* The oauth audience scope, used during token retrieval (either https://api.botframework.com or bot app id if this is a skill calling another skill).
*/
fromBotOAuthScope: string;

/**
* The id of the parent bot that is messaging the skill.
*/
fromBotId: string;

/**
* The activity which will be sent to the skill.
*/
activity: Activity;

/**
* The skill to create the conversation Id for.
*/
botFrameworkSkill: BotFrameworkSkill;
}
14 changes: 14 additions & 0 deletions libraries/botbuilder-core/src/skills/skillConversationReference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @module botbuilder-core
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { ConversationReference } from 'botframework-schema';

export interface SkillConversationReference {
conversationReference: ConversationReference;
oAuthScope: string;
}
66 changes: 59 additions & 7 deletions libraries/botbuilder-dialogs/src/skillDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
Activity,
ActivityTypes,
ConversationReference,
DeliveryModes,
SkillConversationIdFactoryOptions,
TurnContext
} from 'botbuilder-core';
import {
Expand All @@ -25,6 +27,9 @@ import { SkillDialogOptions } from './skillDialogOptions';
export class SkillDialog extends Dialog {
protected dialogOptions: SkillDialogOptions;

// This key uses a simple namespace as Symbols are not serializable.
private readonly DeliveryModeStateKey: string = 'SkillDialog.deliveryMode';

/**
* A sample dialog that can wrap remote calls to a skill.
*
Expand All @@ -43,7 +48,7 @@ export class SkillDialog extends Dialog {
this.dialogOptions = dialogOptions;
}

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

await dc.context.sendTraceActivity(`${ this.id }.beginDialog()`, undefined, undefined, `Using activity of type: ${ dialogArgs.activity.type }`);
Expand All @@ -54,8 +59,14 @@ export class SkillDialog extends Dialog {
// Apply conversation reference and common properties from incoming activity before sending.
const skillActivity = TurnContext.applyConversationReference(clonedActivity, TurnContext.getConversationReference(dc.context.activity), true) as Activity;

// Store the deliveryMode of the first forwarded activity
dc.activeDialog.state[this.DeliveryModeStateKey] = dialogArgs.activity.deliveryMode;

// Send the activity to the skill.
await this.sendToSkill(dc.context, skillActivity);
const eocActivity = await this.sendToSkill(dc.context, skillActivity);
if (eocActivity) {
return await dc.endDialog(eocActivity.value);
}
return Dialog.EndOfTurn;
}

Expand All @@ -71,8 +82,15 @@ export class SkillDialog extends Dialog {

// Forward only Message and Event activities to the skill
if (dc.context.activity.type === ActivityTypes.Message || dc.context.activity.type === ActivityTypes.Event) {
// Create deep clone of the original activity to avoid altering it before forwarding it.
const skillActivity = this.cloneActivity(dc.context.activity);
skillActivity.deliveryMode = dc.activeDialog.state[this.DeliveryModeStateKey] as string;

// Just forward to the remote skill
await this.sendToSkill(dc.context, dc.context.activity);
const eocActivity = await this.sendToSkill(dc.context, skillActivity);
if (eocActivity) {
return await dc.endDialog(eocActivity.value);
}
}

return Dialog.EndOfTurn;
Expand Down Expand Up @@ -122,19 +140,53 @@ export class SkillDialog extends Dialog {
return dialogArgs;
}

private async sendToSkill(context: TurnContext, activity: Activity): Promise<void> {
private async sendToSkill(context: TurnContext, activity: Activity): Promise<Activity> {
// Create a conversationId to interact with the skill and send the activity
const skillConversationId = await this.dialogOptions.conversationIdFactory.createSkillConversationId(TurnContext.getConversationReference(activity) as ConversationReference);
const conversationIdFactoryOptions: SkillConversationIdFactoryOptions = {
fromBotOAuthScope: context.turnState.get(context.adapter.OAuthScopeKey),
fromBotId: this.dialogOptions.botId,
activity: activity,
botFrameworkSkill: this.dialogOptions.skill
};

// Create a conversationId to interact with the skill and send the activity
let skillConversationId: string;
try {
skillConversationId = await this.dialogOptions.conversationIdFactory.createSkillConversationIdWithOptions(conversationIdFactoryOptions);
} catch (err) {
if (err.message !== 'Not Implemented') throw err;
// If the SkillConversationIdFactoryBase implementation doesn't support createSkillConversationIdWithOptions(),
// use createSkillConversationId() instead.
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);
await this.dialogOptions.conversationState.saveChanges(context, true);

const response = await this.dialogOptions.skillClient.postActivity<Activity[]>(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 }`);
}

let eocActivity: Activity;
if (activity.deliveryMode == DeliveryModes.BufferedReplies && response.body) {
// Process replies in the response.Body.
if (Array.isArray(response.body)) {
response.body.forEach(async (fromSkillActivity: Activity): Promise<void> => {
if (fromSkillActivity.type === ActivityTypes.EndOfConversation) {
// Capture the EndOfConversation activity if it was sent from skill
eocActivity = fromSkillActivity;
} else {
await context.sendActivity(fromSkillActivity);
}
});
}
}

return eocActivity;
}
}
2 changes: 1 addition & 1 deletion libraries/botbuilder-dialogs/src/skillDialogOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {

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

Expand Down
Loading