Skip to content

Commit

Permalink
add messages invoke handler
Browse files Browse the repository at this point in the history
  • Loading branch information
aacebo committed Nov 12, 2024
1 parent bf05ad3 commit 8dbb1fc
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 0 deletions.
11 changes: 11 additions & 0 deletions js/packages/teams-ai/src/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ReadReceiptInfo } from 'botframework-connector';
import { AdaptiveCards, AdaptiveCardsOptions } from './AdaptiveCards';
import { AI, AIOptions } from './AI';
import { Meetings } from './Meetings';
import { Messages } from './Messages';
import { MessageExtensions } from './MessageExtensions';
import { TaskModules, TaskModulesOptions } from './TaskModules';
import { AuthenticationManager, AuthenticationOptions } from './authentication/Authentication';
Expand Down Expand Up @@ -243,6 +244,7 @@ export class Application<TState extends TurnState = TurnState> {
private readonly _invokeRoutes: AppRoute<TState>[] = [];
private readonly _adaptiveCards: AdaptiveCards<TState>;
private readonly _meetings: Meetings<TState>;
private readonly _messages: Messages<TState>;
private readonly _messageExtensions: MessageExtensions<TState>;
private readonly _taskModules: TaskModules<TState>;
private readonly _ai?: AI<TState>;
Expand Down Expand Up @@ -285,6 +287,7 @@ export class Application<TState extends TurnState = TurnState> {
}

this._adaptiveCards = new AdaptiveCards<TState>(this);
this._messages = new Messages<TState>(this);
this._messageExtensions = new MessageExtensions<TState>(this);
this._meetings = new Meetings<TState>(this);
this._taskModules = new TaskModules<TState>(this);
Expand Down Expand Up @@ -352,6 +355,14 @@ export class Application<TState extends TurnState = TurnState> {
return this._authentication;
}

/**
* Fluent interface for accessing Messages specific features.
* @returns {Messages<TState>} The Messages instance.
*/
public get messages(): Messages<TState> {
return this._messages;
}

/**
* Fluent interface for accessing Message Extensions' specific features.
* @returns {MessageExtensions<TState>} The MessageExtensions instance.
Expand Down
56 changes: 56 additions & 0 deletions js/packages/teams-ai/src/Messages.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import sinon from 'sinon';
import { strict as assert } from 'assert';
import { ActivityTypes, Channels, INVOKE_RESPONSE_KEY, TestAdapter } from 'botbuilder';

import { Application } from './Application';
import { createTestInvoke } from './internals/testing/TestUtilities';
import { MessageInvokeNames, Messages } from './Messages';

describe('Messages', () => {
const adapter = new TestAdapter();
let mockApp: Application;

beforeEach(() => {
mockApp = new Application();
sinon.stub(mockApp, 'adapter').get(() => adapter);
});

it('should exist when Application is instantiated', () => {
assert.notEqual(mockApp.messages, undefined);
assert.equal(mockApp.messages instanceof Messages, true);
});

describe(MessageInvokeNames.FETCH_INVOKE_NAME, () => {
it('fetch() with custom RouteSelector handler result is falsy', async () => {
const activity = createTestInvoke(MessageInvokeNames.FETCH_INVOKE_NAME, {});
activity.channelId = Channels.Msteams;
mockApp.messages.fetch(async (_context, _state, _data) => {
return {};
});

await adapter.processActivity(activity, async (context) => {
await mockApp.run(context);
const response = context.turnState.get(INVOKE_RESPONSE_KEY);
assert.deepEqual(response.value, {
status: 200,
body: { task: { type: 'continue', value: {} } }
});
});
});

it('fetch() with custom RouteSelector unhappy path', async () => {
const activity = { channelId: Channels.Msteams, type: ActivityTypes.Invoke, name: 'incorrectName' };
const spy = sinon.spy(async (context, _state, _data) => {
return Promise.resolve('');
});

mockApp.messages.fetch(spy);

await adapter.processActivity(activity, async (context) => {
await mockApp.run(context);
});

assert.equal(spy.called, false);
});
});
});
100 changes: 100 additions & 0 deletions js/packages/teams-ai/src/Messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* @module teams-ai
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import {
ActivityTypes,
Channels,
INVOKE_RESPONSE_KEY,
InvokeResponse,
TaskModuleResponse,
TaskModuleTaskInfo,
TurnContext
} from 'botbuilder';
import { Application } from './Application';
import { TurnState } from './TurnState';

export enum MessageInvokeNames {
FETCH_INVOKE_NAME = `message/fetchTask`
}

/**
* TaskModules class to enable fluent style registration of handlers related to Task Modules.
* @template TState Type of the turn state object being persisted.
*/
export class Messages<TState extends TurnState> {
private readonly _app: Application<TState>;

/**
* Creates a new instance of the TaskModules class.
* @param {Application} app Top level application class to register handlers with.
*/
public constructor(app: Application<TState>) {
this._app = app;
}

/**
* Registers a handler to process the initial fetch of the task module.
* @remarks
* Handlers should respond with either an initial TaskInfo object or a string containing
* a message to display to the user.
* @template TData Optional. Type of the data object being passed to the handler.
* @param {(context: TurnContext, state: TState, data: TData) => Promise<TaskModuleTaskInfo | string>} handler - Function to call when the handler is triggered.
* @param {TurnContext} handler.context - Context for the current turn of conversation with the user.
* @param {TState} handler.state - Current state of the turn.
* @param {TData} handler.data - Data object passed to the handler.
* @returns {Application<TState>} The application for chaining purposes.
*/
public fetch<TData extends Record<string, any> = Record<string, any>>(
handler: (context: TurnContext, state: TState, data: TData) => Promise<TaskModuleTaskInfo | string>
): Application<TState> {
this._app.addRoute(
async (context) => {
return (
context?.activity?.type === ActivityTypes.Invoke &&
context?.activity?.name === MessageInvokeNames.FETCH_INVOKE_NAME
);
},
async (context, state) => {
if (context?.activity?.channelId === Channels.Msteams) {
const result = await handler(context, state, context.activity.value?.data ?? {});

if (!context.turnState.get(INVOKE_RESPONSE_KEY)) {
// Format invoke response
let response: TaskModuleResponse;
if (typeof result == 'string') {
// Return message
response = {
task: {
type: 'message',
value: result
}
};
} else {
// Return card
response = {
task: {
type: 'continue',
value: result
}
};
}

// Queue up invoke response
await context.sendActivity({
value: { body: response, status: 200 } as InvokeResponse,
type: ActivityTypes.InvokeResponse
});
}
}
},
true
);

return this._app;
}
}

0 comments on commit 8dbb1fc

Please sign in to comment.