Skip to content
This repository has been archived by the owner on Sep 20, 2024. It is now read-only.

Support entire message objects in convo handlers. Fixes #1793 and #1710 #1801

Closed
wants to merge 13 commits into from
Closed
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
2 changes: 1 addition & 1 deletion packages/botkit/src/botworker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export class BotWorker {
let activity = this.ensureMessageFormat(resp);

// Get conversation reference from src
const reference = TurnContext.getConversationReference(src.incoming_message);
const reference = src.reference;

activity = TurnContext.applyConversationReference(activity, reference);

Expand Down
31 changes: 17 additions & 14 deletions packages/botkit/src/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Botkit } from './core';
import { Botkit, BotkitMessage } from './core';
import { BotWorker } from './botworker';
import { BotkitDialogWrapper } from './dialogWrapper';
import { ActivityTypes, TurnContext, MessageFactory, ActionTypes } from 'botbuilder';
import { Dialog, DialogContext, DialogReason, TextPrompt, DialogTurnStatus } from 'botbuilder-dialogs';
import { Activity, ActivityTypes, TurnContext, MessageFactory, ActionTypes } from 'botbuilder';
import { Dialog, DialogContext, DialogReason, PromptValidatorContext, ActivityPrompt, DialogTurnStatus } from 'botbuilder-dialogs';
import * as mustache from 'mustache';
import * as Debug from 'debug';

Expand All @@ -19,7 +19,7 @@ const debug = Debug('botkit:conversation');
* Definition of the handler functions used to handle .ask and .addQuestion conditions
*/
interface BotkitConvoHandler {
(answer: string, convo: BotkitDialogWrapper, bot: BotWorker): Promise<any>;
(answer: string, convo: BotkitDialogWrapper, bot: BotWorker, message: BotkitMessage): Promise<any>;
}

/**
Expand Down Expand Up @@ -142,7 +142,10 @@ export class BotkitConversation<O extends object = {}> extends Dialog<O> {
// Make sure there is a prompt we can use.
// TODO: maybe this ends up being managed by Botkit
this._prompt = this.id + '_default_prompt';
this._controller.dialogSet.add(new TextPrompt(this._prompt));
this._controller.dialogSet.add(new ActivityPrompt(
this._prompt,
(prompt: PromptValidatorContext<Activity>) => Promise.resolve(prompt.recognized.succeeded === true)
));

return this;
}
Expand Down Expand Up @@ -477,7 +480,6 @@ export class BotkitConversation<O extends object = {}> extends Dialog<O> {
const bot = await this._controller.spawn(context);
for (let h = 0; h < this._afterHooks.length; h++) {
const handler = this._afterHooks[h];

await handler.call(this, results, bot);
}
}
Expand Down Expand Up @@ -525,7 +527,6 @@ export class BotkitConversation<O extends object = {}> extends Dialog<O> {

for (let h = 0; h < this._changeHooks[variable].length; h++) {
const handler = this._changeHooks[variable][h];
// await handler.call(this, value, convo);
await handler.call(this, value, convo, bot);
}
}
Expand Down Expand Up @@ -559,7 +560,7 @@ export class BotkitConversation<O extends object = {}> extends Dialog<O> {
}

// Run next step with the message text as the result.
return await this.resumeDialog(dc, DialogReason.continueCalled, dc.context.activity.text);
return await this.resumeDialog(dc, DialogReason.continueCalled, dc.context.activity);
}

/**
Expand Down Expand Up @@ -679,8 +680,8 @@ export class BotkitConversation<O extends object = {}> extends Dialog<O> {
await dc.context.sendActivity(`Failed to start prompt ${ this._prompt }`);
return await step.next();
}
// If there's nothing but text, send it!
// This could be extended to include cards and other activity attributes.
// If there's nothing but text, send it!
// This could be extended to include cards and other activity attributes.
} else {
// if there is text, attachments, or any channel data fields at all...
if (line.type || line.text || line.attachments || line.attachment || line.blocks || (line.channelData && Object.keys(line.channelData).length)) {
Expand Down Expand Up @@ -717,7 +718,6 @@ export class BotkitConversation<O extends object = {}> extends Dialog<O> {
const state = dc.activeDialog.state;
state.stepIndex = index;
state.thread = thread_name;

// Create step context
const nextCalled = false;
const step = {
Expand All @@ -727,7 +727,8 @@ export class BotkitConversation<O extends object = {}> extends Dialog<O> {
state: state,
options: state.options,
reason: reason,
result: result,
result: result && result.text ? result.text : result,
resultObject: result,
values: state.values,
next: async (stepResult): Promise<any> => {
if (nextCalled) {
Expand Down Expand Up @@ -808,7 +809,7 @@ export class BotkitConversation<O extends object = {}> extends Dialog<O> {

outgoing.channelData = outgoing.channelData ? outgoing.channelData : {};
if (line.attachmentLayout) {
outgoing.attachmentLayout = line.attachmentLayout;
outgoing.attachmentLayout = line.attachmentLayout;
}
/*******************************************************************************************************************/
// allow dynamic generation of quick replies and/or attachments
Expand Down Expand Up @@ -958,14 +959,16 @@ export class BotkitConversation<O extends object = {}> extends Dialog<O> {
if (path.handler) {
const index = step.index;
const thread_name = step.thread;
const result = step.result;
const response = result.text || (typeof (result) === 'string' ? result : null);

// spawn a bot instance so devs can use API or other stuff as necessary
const bot = await this._controller.spawn(dc);

// create a convo controller object
const convo = new BotkitDialogWrapper(dc, step);

await path.handler.call(this, step.result, convo, bot);
await path.handler.call(this, response, convo, bot, step.resultObject);

if (!dc.activeDialog) {
return false;
Expand Down
65 changes: 54 additions & 11 deletions packages/botkit/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Activity, MemoryStorage, Storage, ConversationReference, TurnContext } from 'botbuilder';
import { Activity, ActivityTypes, MemoryStorage, Storage, ConversationReference, TurnContext } from 'botbuilder';
import { Dialog, DialogContext, DialogSet, DialogTurnStatus, WaterfallDialog } from 'botbuilder-dialogs';
import { BotkitBotFrameworkAdapter } from './adapter';
import { BotWorker } from './botworker';
Expand Down Expand Up @@ -79,16 +79,21 @@ export interface BotkitConfiguration {
* Defines the expected form of a message or event object being handled by Botkit.
* Will also contain any additional fields including in the incoming payload.
*/
export interface BotkitMessage {
export interface BotkitMessage extends Activity {
/**
* The type of event, in most cases defined by the messaging channel or adapter
*/
type: string;

/**
* The event name if the type is 'event'
*/
event?: string;

/**
* Text of the message sent by the user (or primary value in case of button click)
*/
text?: string;
text: string;

/**
* Any value field received from the platform
Expand All @@ -109,7 +114,7 @@ export interface BotkitMessage {
* A full [ConversationReference](https://docs.microsoft.com/en-us/javascript/api/botframework-schema/conversationreference?view=botbuilder-ts-latest) object that defines the address of the message and all information necessary to send messages back to the originating location.
* Can be stored for later use, and used with [bot.changeContext()](#changeContext) to send proactive messages.
*/
reference: ConversationReference;
reference: Partial<ConversationReference>;

/**
* The original incoming [BotBuilder Activity](https://docs.microsoft.com/en-us/javascript/api/botframework-schema/activity?view=botbuilder-ts-latest) object as created by the adapter.
Expand Down Expand Up @@ -689,6 +694,35 @@ export class Botkit {
}
}

/**
* Converts an incoming message into BotkitMessage activity
* @param message The incoing message from adapter
* @return {BotkitMessage} the newly created BotkitMessage activity
*/
private incomingMessageToBotkitMessage(message: any): BotkitMessage {
const { channelData } = message;
const activity = {
// start with all the fields that were in the original incoming payload. NOTE: this is a shallow copy, is that a problem?
...message,
// timestamp: new Date(),
type: message.type === 'message' ? ActivityTypes.Message : ActivityTypes.Event,
// normalize the user, text and channel info
event: channelData.type || channelData.botkitEventType || message.type,
text: message.text,
user: message.from.id,
channel: message.conversation.id,
value: message.value,
incoming_messge: message,

Choose a reason for hiding this comment

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

Should this not be incoming_message?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes good catch I will probably not merge this piece anyways since it is sort of out of scope of the main change.

channelData: message.channelData
};

// set botkit's event type
if (activity.type !== ActivityTypes.Message) {
activity.channelData.botkitEventType = message.type;
}
return activity;
}

/**
* Accepts the result of a BotBuilder adapter's `processActivity()` method and processes it into a Botkit-style message and BotWorker instance
* which is then used to test for triggers and emit events.
Expand All @@ -698,13 +732,21 @@ export class Botkit {
public async handleTurn(turnContext: TurnContext): Promise<any> {
debug('INCOMING ACTIVITY:', turnContext.activity);

const botkitContext = new TurnContext(
turnContext.adapter,
this.incomingMessageToBotkitMessage(turnContext.activity)
);

// Create a dialog context
const dialogContext = await this.dialogSet.createContext(turnContext);
const dialogContext = await this.dialogSet.createContext(botkitContext);

// Spawn a bot worker with the dialogContext
const bot = await this.spawn(dialogContext);

// Turn this turnContext into a Botkit message.
const message = botkitContext.activity as BotkitMessage;
message.reference = TurnContext.getConversationReference(message);
/*
const message: BotkitMessage = {
...turnContext.activity.channelData, // start with all the fields that were in the original incoming payload. NOTE: this is a shallow copy, is that a problem?

Expand All @@ -726,8 +768,9 @@ export class Botkit {
context: turnContext,

// include the full unmodified record here
incoming_message: turnContext.activity
// incoming_message: turnContext.activity
};
*/

return new Promise((resolve, reject) => {
this.middleware.ingest.run(bot, message, async (err, bot, message) => {
Expand Down Expand Up @@ -785,7 +828,7 @@ export class Botkit {
resolve(listen_results);
} else {
// Trigger event handlers
const trigger_results = await this.trigger(message.type, bot, message);
const trigger_results = await this.trigger(message.event, bot, message);

resolve(trigger_results);
}
Expand All @@ -799,8 +842,8 @@ export class Botkit {
* @param message {BotkitMessage} an incoming message
*/
private async listenForTriggers(bot: BotWorker, message: BotkitMessage): Promise<any> {
if (this._triggers[message.type]) {
const triggers = this._triggers[message.type];
if (this._triggers[message.event]) {
const triggers = this._triggers[message.event];
for (let t = 0; t < triggers.length; t++) {
const test_results = await this.testTrigger(triggers[t], message);
if (test_results) {
Expand All @@ -823,8 +866,8 @@ export class Botkit {
* @param message {BotkitMessage} an incoming message
*/
private async listenForInterrupts(bot: BotWorker, message: BotkitMessage): Promise<any> {
if (this._interrupts[message.type]) {
const triggers = this._interrupts[message.type];
if (this._interrupts[message.event]) {
const triggers = this._interrupts[message.event];
for (let t = 0; t < triggers.length; t++) {
const test_results = await this.testTrigger(triggers[t], message);
if (test_results) {
Expand Down
92 changes: 92 additions & 0 deletions packages/botkit/tests/Dialog.tests.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const assert = require('assert');
const { ActivityTypes } = require('botbuilder');
const { Botkit, BotkitTestClient, BotkitConversation } = require('../');

let bot;
Expand Down Expand Up @@ -543,6 +544,97 @@ describe('Botkit dialog', function() {
assert(after_fired === true, 'after fired after stop');

});

it('should call handlers for addQuestion and ask with entire message payload', async () => {
const conversation = new BotkitConversation('nameConvo', bot);

let correct1 = false;
let correct2 = false;
let correct3 = false;
let correct4 = false;
let correct5 = false;

conversation.ask(
'First name?',
async (response, convo, bot, message) => {
correct1 = message.type === ActivityTypes.Message && message.text === 'Tony' && response === message.text;
convo.gotoThread('last_name');
},
'firstName'
);
conversation.addQuestion(
'Last name?',
async (response, convo, bot, message) => {
correct2 = message.type === ActivityTypes.Message && message.text === 'Stark' && response === message.text;
convo.gotoThread('address');
},
'lastName',
'last_name'
);

conversation.addQuestion(
'Address?',
async (response, convo, bot, message) => {
correct3 = message.type === ActivityTypes.Message &&
message.text === '10880 Malibu Point, 90265, Malibu, California' &&
response === message.text;
convo.gotoThread('color');
},
'address',
'address'
);
conversation.addQuestion(
'Favourite Color?',
[
{
default: true,
handler: async (response, convo, bot, message) => {
correct4 = message.type === ActivityTypes.Message && response === message.text;
await convo.repeat();
}
},
{
pattern: 'Red',
handler: async (response, convo, bot, message) => {
correct5 = message.type === ActivityTypes.Message &&
message.text === 'Red' && response === message.text;
await convo.stop();
}
}
],
'color',
'color'
);

bot.addDialog(conversation);

// set up a test client
const client = new BotkitTestClient('test', bot, ['nameConvo']);

let msg = await client.sendActivity('..');
assert(msg.text === 'First name?', 'first prompt wrong');

msg = await client.sendActivity('Tony');
assert(msg.text === 'Last name?', 'second prompt wrong');

msg = await client.sendActivity('Stark');
assert(msg.text === 'Address?', 'third prompt wrong');

msg = await client.sendActivity('10880 Malibu Point, 90265, Malibu, California');
assert(msg.text === 'Favourite Color?', 'fourth prompt wrong');

msg = await client.sendActivity('Black');
assert(msg.text === 'Favourite Color?', 'repeat prompt wrong');

msg = await client.sendActivity('Red');

assert(correct1, 'message text not correct for prompt1');
assert(correct2, 'message text not correct for prompt2');
assert(correct3, 'message text not correct for prompt3');
assert(correct4, 'message text not correct for prompt4');
assert(correct5, 'message text not correct for prompt5');
});

afterEach(async () => {
await bot.shutdown();
});
Expand Down