Skip to content

Commit 1c0caa7

Browse files
committed
add skillDialog and associated classes
1 parent 80ebf56 commit 1c0caa7

File tree

3 files changed

+180
-0
lines changed

3 files changed

+180
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* @module botbuilder-dialogs
3+
*/
4+
/**
5+
* Copyright (c) Microsoft Corporation. All rights reserved.
6+
* Licensed under the MIT License.
7+
*/
8+
9+
import { BotFrameworkSkill } from 'botbuilder'; // bad import
10+
import { Activity, ActivityTypes, ConversationState, StatePropertyAccessor, TurnContext } from 'botbuilder-core';
11+
import { Dialog, DialogTurnResult } from './dialog';
12+
import { DialogContext } from './dialogContext';
13+
import { SkillDialogArgs } from './skillDialogArgs';
14+
import { SkillDialogOptions } from './skillDialogOptions';
15+
16+
export class SkillDialog extends Dialog {
17+
private readonly _activeSkillProperty: StatePropertyAccessor;
18+
private readonly _conversationState: ConversationState; // Why is this restricted to ConversationState?
19+
private readonly _dialogOptions: SkillDialogOptions;
20+
21+
/**
22+
* A sample dialog that can wrap remote calls to a skill.
23+
*
24+
* @remarks
25+
* The options parameter in `beginDialog()` must be a `SkillDialogArgs` object with the initial parameters
26+
* for the dialog.
27+
*
28+
* @param SkillDialogOptions
29+
* @param dialogOptions
30+
* @param ConversationState
31+
* @param conversationState
32+
*/
33+
public constructor(dialogOptions: SkillDialogOptions, conversationState: ConversationState) {
34+
super('SkillDialog'); // What about Dialog Id?
35+
if (!dialogOptions) {
36+
throw new TypeError('Missing dialogOptions parameter');
37+
}
38+
39+
if (!conversationState) {
40+
throw new TypeError('Missing conversationState parameter');
41+
}
42+
43+
this._dialogOptions = dialogOptions;
44+
this._conversationState = conversationState;
45+
46+
this._activeSkillProperty = conversationState.createProperty<BotFrameworkSkill>('botbuilder-dialogs.SkillDialog.ActiveSkillProperty');
47+
}
48+
49+
public async beginDialog(dc: DialogContext, options?: {}): Promise<DialogTurnResult> {
50+
const dialogArgs = SkillDialog.validateBeginDialogOptions(options);
51+
52+
await dc.context.sendTraceActivity(`${ this.id }.beginDialog()`, undefined, undefined, `Using activity of type: ${ dialogArgs.activity.type }`);
53+
54+
// Store Skill information for this dialog instance
55+
await this._activeSkillProperty.set(dc.context, dialogArgs.skill);
56+
57+
// Create deep clone of the original activity to avoid altering it before forwarding it.
58+
const clonedActivity = this.cloneActivity(dialogArgs.activity);
59+
60+
// Apply conversation reference and common properties from incoming activity before sending.
61+
const skillActivity = TurnContext.applyConversationReference(clonedActivity, TurnContext.getConversationReference(dc.context.activity), true) as Activity;
62+
63+
// Send the activity to the skill.
64+
await this.sendToSkill(dc, skillActivity, dialogArgs.skill);
65+
return Dialog.EndOfTurn;
66+
}
67+
68+
public async continueDialog(dc: DialogContext): Promise<DialogTurnResult> {
69+
await dc.context.sendTraceActivity(`${ this.id }.continueDialog()`, undefined, undefined, `ActivityType: ${ dc.context.activity.type }`);
70+
71+
// Retrieve the current skill information from ConversationState
72+
var skillInfo = await this._activeSkillProperty.get(dc.context, null);
73+
74+
// Handle EndOfConversation from the skill (this will be sent to the this dialog by the SkillHandler if received from the Skill)
75+
if (dc.context.activity.type === ActivityTypes.EndOfConversation) {
76+
await dc.context.sendTraceActivity(`${ this.id }.continueDialog()`, undefined, undefined, `Got ${ ActivityTypes.EndOfConversation }`);
77+
return await dc.endDialog(dc.context.activity.value);
78+
}
79+
80+
// Forward only Message and Event activities to the skill
81+
if (dc.context.activity.type === ActivityTypes.Message || dc.context.activity.type === ActivityTypes.Event) {
82+
// Just forward to the remote skill
83+
await this.sendToSkill(dc, dc.context.activity, skillInfo);
84+
}
85+
86+
return Dialog.EndOfTurn;
87+
}
88+
89+
/**
90+
* Clones the Activity entity.
91+
* @param activity Activity to clone.
92+
*/
93+
private cloneActivity(activity: Partial<Activity>): Activity {
94+
return Object.assign({} as Activity, activity);
95+
}
96+
97+
private static validateBeginDialogOptions(options: any): SkillDialogArgs {
98+
if (!options) {
99+
throw new TypeError('Missing options parameter');
100+
}
101+
102+
const dialogArgs = options as SkillDialogArgs;
103+
104+
if (dialogArgs.skill == undefined || dialogArgs.skill == null) {
105+
throw new TypeError(`"skill" undefined or null in options`);
106+
}
107+
108+
if (dialogArgs.activity === undefined || dialogArgs.activity === null) {
109+
throw new TypeError(`"activity" is undefined or null in options.`);
110+
}
111+
112+
// Only accept Message or Event activities
113+
if (dialogArgs.activity.type !== ActivityTypes.Message && dialogArgs.activity.type !== ActivityTypes.Event) {
114+
// Just forward to the remote skill
115+
throw new TypeError(`Only ${ ActivityTypes.Message } and ${ ActivityTypes.Event } activities are supported. Received activity of type ${ dialogArgs.activity.type } in options.`);
116+
}
117+
118+
return dialogArgs;
119+
}
120+
121+
private async sendToSkill(dc: DialogContext, activity: Activity, skillInfo: BotFrameworkSkill): Promise<void> {
122+
// Always save state before forwarding
123+
// (the dialog stack won't get updated with the skillDialog and things won't work if you don't)
124+
await this._conversationState.saveChanges(dc.context, true);
125+
126+
// Create a conversationId to interact with the skill and send the activity
127+
const skillConversationId = await this._dialogOptions.conversationIdFactory.createSkillConversationId(TurnContext.getConversationReference(activity));
128+
const response = await this._dialogOptions.skillClient.postActivity(this._dialogOptions.botId, skillInfo.AppId, skillInfo.SkillEndpoint, this._dialogOptions.skillHostEndpoint, skillConversationId, activity);
129+
130+
// Inspect the skill response status
131+
if (!(response.status >= 200 && response.status <= 299)) {
132+
throw new Error(`Error invoking the skill id: "${ skillInfo.id }" at "${ skillInfo.skillEndpoint }" (status is ${ response.status }). \r\n ${ response.body }`);
133+
}
134+
}
135+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @module botbuilder-dialogs
3+
*/
4+
/**
5+
* Copyright (c) Microsoft Corporation. All rights reserved.
6+
* Licensed under the MIT License.
7+
*/
8+
9+
import { BotFrameworkSkill } from 'botbuilder'; // bad import
10+
import { Activity } from 'botbuilder-core';
11+
12+
/**
13+
* A class with dialog arguments for a SkillDialog.
14+
*/
15+
export interface SkillDialogArgs {
16+
/**
17+
* The BotFrameworkSkill that the dialog will call.
18+
*/
19+
skill: BotFrameworkSkill;
20+
21+
/**
22+
* The Activity to send to the skill.
23+
*/
24+
activity: Activity;
25+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @module botbuilder-dialogs
3+
*/
4+
/**
5+
* Copyright (c) Microsoft Corporation. All rights reserved.
6+
* Licensed under the MIT License.
7+
*/
8+
9+
import { BotFrameworkHttpClient, SkillConversationIdFactoryBase } from 'botbuilder'; // bad import
10+
11+
export interface SkillDialogOptions {
12+
13+
botId: string;
14+
15+
skillClient: BotFrameworkHttpClient;
16+
17+
skillHostEndpoint: string;
18+
19+
conversationIdFactory: SkillConversationIdFactoryBase;
20+
}

0 commit comments

Comments
 (0)