@microsoft/teams-ai
Version:
SDK focused on building AI based applications for Microsoft Teams.
560 lines • 30.5 kB
JavaScript
;
/**
* @module teams-ai
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.MessageExtensions = exports.MessageExtensionsInvokeNames = void 0;
const botbuilder_1 = require("botbuilder");
/**
* Names of the invoke activities for Message Extensions.
*/
var MessageExtensionsInvokeNames;
(function (MessageExtensionsInvokeNames) {
/**
* Name of the invoke activity for anonymous link unfurling.
*/
MessageExtensionsInvokeNames["ANONYMOUS_QUERY_LINK_INVOKE"] = "composeExtension/anonymousQueryLink";
/**
* Name of the invoke activity for fetching task module.
*/
MessageExtensionsInvokeNames["FETCH_TASK_INVOKE"] = "composeExtension/fetchTask";
/**
* Name of the invoke activity for query.
*/
MessageExtensionsInvokeNames["QUERY_INVOKE"] = "composeExtension/query";
/**
* Name of the invoke activity for query link.
*/
MessageExtensionsInvokeNames["QUERY_LINK_INVOKE"] = "composeExtension/queryLink";
/**
* Name of the invoke activity for selecting an item.
*/
MessageExtensionsInvokeNames["SELECT_ITEM_INVOKE"] = "composeExtension/selectItem";
/**
* Name of the invoke activity for submit action.
*/
MessageExtensionsInvokeNames["SUBMIT_ACTION_INVOKE"] = "composeExtension/submitAction";
/**
* Name of the invoke activity for querying configuration settings.
*/
MessageExtensionsInvokeNames["QUERY_SETTING_URL"] = "composeExtension/querySettingUrl";
/**
* Name of the invoke activity for configuring settings.
*/
MessageExtensionsInvokeNames["CONFIGURE_SETTINGS"] = "composeExtension/setting";
/**
* Name of the invoke activity for handling on card button clicked.
*/
MessageExtensionsInvokeNames["QUERY_CARD_BUTTON_CLICKED"] = "composeExtension/onCardButtonClicked";
})(MessageExtensionsInvokeNames || (exports.MessageExtensionsInvokeNames = MessageExtensionsInvokeNames = {}));
/**
* MessageExtensions class to enable fluent style registration of handlers related to Message Extensions.
* @template TState Type of the turn state object being persisted.
*/
class MessageExtensions {
_app;
/**
* Creates a new instance of the MessageExtensions class.
* @param {Application} app Top level application class to register handlers with.
*/
constructor(app) {
this._app = app;
}
/**
* Registers a handler for a command that performs anonymous link unfurling.
* @remarks
* The `composeExtension/anonymousQueryLink` INVOKE activity does not contain any sort of command ID,
* so only a single select item handler can be registered.
* For more information visit https://learn.microsoft.com/microsoftteams/platform/messaging-extensions/how-to/link-unfurling?#enable-zero-install-link-unfurling
* @param {(context: TurnContext, state: TState, url: string) => Promise<MessagingExtensionResult>} handler - Function to call when the command is received. The handler should return a `MessagingExtensionResult`.
* @param {TurnContext} handler.context - Context for the current turn of conversation with the user.
* @param {TState} handler.state - Current state of the turn.
* @param {string} handler.url - URL to unfurl.
* @returns {Application<TState>} The application for chaining purposes.
*/
anonymousQueryLink(handler) {
const { ANONYMOUS_QUERY_LINK_INVOKE } = MessageExtensionsInvokeNames;
const selector = (context) => Promise.resolve(context?.activity?.type == botbuilder_1.ActivityTypes.Invoke &&
context?.activity.name === ANONYMOUS_QUERY_LINK_INVOKE);
this._app.addRoute(selector, async (context, state) => {
// Call handler and then check to see if an invoke response has already been added
const result = await handler(context, state, context.activity.value?.url ?? '');
if (!context.turnState.get(botbuilder_1.INVOKE_RESPONSE_KEY)) {
// Format invoke response
const response = {
composeExtension: result
};
// Queue up invoke response
await context.sendActivity({
value: { body: response, status: 200 },
type: botbuilder_1.ActivityTypes.InvokeResponse
});
}
}, true);
return this._app;
}
/**
* Registers a handler to process the 'edit' action of a message that's being previewed by the
* user prior to sending.
* @remarks
* This handler is called when the user clicks the 'Edit' button on a message that's being
* previewed prior to insertion into the current chat. The handler should return a new
* view that allows the user to edit the message.
* @param {string | RegExp | RouteSelector | string[] | RegExp[] | RouteSelector[]} commandId - ID of the command(s) to register the handler for.
* @param {(context: TurnContext, state: TState, previewActivity: Partial<Activity>) => Promise<MessagingExtensionResult | TaskModuleTaskInfo | string | null | undefined>} handler - Function to call when the command is received.
* @param {TurnContext} handler.context - Context for the current turn of conversation with the user.
* @param {TState} handler.state - Current state of the turn.
* @param {Partial<Activity>} handler.previewActivity - The activity that's being previewed by the user.
* @returns {Application<TState>} The application for chaining purposes.
*/
botMessagePreviewEdit(commandId, handler) {
const { SUBMIT_ACTION_INVOKE } = MessageExtensionsInvokeNames;
(Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => {
const selector = createTaskSelector(cid, SUBMIT_ACTION_INVOKE, 'edit');
this._app.addRoute(selector, async (context, state) => {
// Insure that we're in an invoke as expected
if (context?.activity?.type !== botbuilder_1.ActivityTypes.Invoke ||
context?.activity?.name !== SUBMIT_ACTION_INVOKE ||
context?.activity?.value?.botMessagePreviewAction !== 'edit') {
throw new Error(`Unexpected MessageExtensions.botMessagePreviewEdit() triggered for activity type: ${context?.activity?.type}`);
}
// Call handler and then check to see if an invoke response has already been added
const result = await handler(context, state, context.activity.value?.botActivityPreview[0] ?? {});
await this.returnSubmitActionResponse(context, result);
}, true);
});
return this._app;
}
/**
* Registers a handler to process the 'send' action of a message that's being previewed by the
* user prior to sending.
* @remarks
* This handler is called when the user clicks the 'Send' button on a message that's being
* previewed prior to insertion into the current chat. The handler should complete the flow
* by sending the message to the current chat.
* @param {string | RegExp | RouteSelector | string[] | RegExp[] | RouteSelector[]} commandId - ID of the command(s) to register the handler for.
* @param {(context: TurnContext, state: TState, previewActivity: Partial<Activity>) => Promise<void>} handler - Function to call when the command is received.
* @param {TurnContext} handler.context - Context for the current turn of conversation with the user.
* @param {TState} handler.state - Current state of the turn.
* @param {Partial<Activity>} handler.previewActivity - The activity that's being previewed by the user.
* @returns {Application<TState>} The application for chaining purposes.
*/
botMessagePreviewSend(commandId, handler) {
const { SUBMIT_ACTION_INVOKE } = MessageExtensionsInvokeNames;
(Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => {
const selector = createTaskSelector(cid, SUBMIT_ACTION_INVOKE, 'send');
this._app.addRoute(selector, async (context, state) => {
// Insure that we're in an invoke as expected
if (context?.activity?.type !== botbuilder_1.ActivityTypes.Invoke ||
context?.activity?.name !== SUBMIT_ACTION_INVOKE ||
context?.activity?.value?.botMessagePreviewAction !== 'send') {
throw new Error(`Unexpected MessageExtensions.botMessagePreviewSend() triggered for activity type: ${context?.activity?.type}`);
}
// Call handler and then check to see if an invoke response has already been added
await handler(context, state, context.activity.value?.botActivityPreview[0] ?? {});
// Queue up invoke response
if (!context.turnState.get(botbuilder_1.INVOKE_RESPONSE_KEY)) {
await context.sendActivity({
value: { body: {}, status: 200 },
type: botbuilder_1.ActivityTypes.InvokeResponse
});
}
}, true);
});
return this._app;
}
/**
* Registers a handler to process the initial fetch task for an Action based message extension.
* @remarks
* Handlers should response with either an initial TaskInfo object or a string containing
* a message to display to the user.
* @param {string | RegExp | RouteSelector | string[] | RegExp[] | RouteSelector[]} commandId - ID of the command(s) to register the handler for.
* @param {(context: TurnContext, state: TState) => Promise<TaskModuleTaskInfo | string>} handler - Function to call when the command is received.
* @param {TurnContext} handler.context - Context for the current turn of conversation with the user.
* @param {TState} handler.state - Current state of the turn.
* @returns {Application<TState>} The application for chaining purposes.
*/
fetchTask(commandId, handler) {
const { FETCH_TASK_INVOKE } = MessageExtensionsInvokeNames;
(Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => {
const selector = createTaskSelector(cid, FETCH_TASK_INVOKE);
this._app.addRoute(selector, async (context, state) => {
// Insure that we're in an invoke as expected
if (context?.activity?.type !== botbuilder_1.ActivityTypes.Invoke ||
context?.activity?.name !== FETCH_TASK_INVOKE) {
throw new Error(`Unexpected MessageExtensions.fetchTask() triggered for activity type: ${context?.activity?.type}`);
}
// Call handler and then check to see if an invoke response has already been added
const result = await handler(context, state);
if (!context.turnState.get(botbuilder_1.INVOKE_RESPONSE_KEY)) {
// Format invoke response
let response;
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 },
type: botbuilder_1.ActivityTypes.InvokeResponse
});
}
}, true);
});
return this._app;
}
/**
* Registers a handler that implements a Search based Message Extension.
* @remarks
* This handler is called when the user submits a query to a Search based Message Extension.
* The handler should return a MessagingExtensionResult containing the results of the query.
* @param {string | RegExp | RouteSelector | string[] | RegExp[] | RouteSelector[]} commandId - ID of the command(s) to register the handler for.
* @template TParams
* @param {(context: TurnContext, state: TState, query: Query<TParams>) => Promise<MessagingExtensionResult>} handler - Function to call when the command is received.
* @param {TurnContext} handler.context - Context for the current turn of conversation with the user.
* @param {TState} handler.state - Current state of the turn.
* @param {Query<TParams>} handler.query - The query parameters that were sent by the client.
* @returns {Application<TState>} The application for chaining purposes.
*/
query(commandId, handler) {
const { QUERY_INVOKE } = MessageExtensionsInvokeNames;
(Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => {
const selector = createTaskSelector(cid, QUERY_INVOKE);
this._app.addRoute(selector, async (context, state) => {
// Insure that we're in an invoke as expected
if (context?.activity?.type !== botbuilder_1.ActivityTypes.Invoke || context?.activity?.name !== QUERY_INVOKE) {
throw new Error(`Unexpected MessageExtensions.query() triggered for activity type: ${context?.activity?.type}`);
}
// Flatten query options
const meQuery = context?.activity?.value ?? {};
const query = {
count: meQuery?.queryOptions?.count ?? 25,
skip: meQuery?.queryOptions?.skip ?? 0,
parameters: {}
};
// Flatten query parameters
(meQuery.parameters ?? []).forEach((param) => {
if (param.name) {
query.parameters[param.name] = param.value;
}
});
// Call handler and then check to see if an invoke response has already been added
const result = await handler(context, state, query);
if (!context.turnState.get(botbuilder_1.INVOKE_RESPONSE_KEY)) {
// Format invoke response
const response = {
composeExtension: result
};
// Queue up invoke response
await context.sendActivity({
value: { body: response, status: 200 },
type: botbuilder_1.ActivityTypes.InvokeResponse
});
}
}, true);
});
return this._app;
}
/**
* Registers a handler that implements a Link Unfurling based Message Extension.
* @param {(context: TurnContext, state: TState, url: string) => Promise<MessagingExtensionResult>} handler - Function to call when the command is received.
* @param {TurnContext} handler.context - Context for the current turn of conversation with the user.
* @param {TState} handler.state - Current state of the turn.
* @param {string} handler.url - The URL that should be unfurled.
* @returns {Application<TState>} The application for chaining purposes.
*/
queryLink(handler) {
const { QUERY_LINK_INVOKE } = MessageExtensionsInvokeNames;
const selector = (context) => Promise.resolve(context?.activity?.type == botbuilder_1.ActivityTypes.Invoke && context?.activity.name === QUERY_LINK_INVOKE);
this._app.addRoute(selector, async (context, state) => {
// Call handler and then check to see if an invoke response has already been added
const result = await handler(context, state, context.activity.value?.url);
if (!context.turnState.get(botbuilder_1.INVOKE_RESPONSE_KEY)) {
// Format invoke response
const response = {
composeExtension: result
};
// Queue up invoke response
await context.sendActivity({
value: { body: response, status: 200 },
type: botbuilder_1.ActivityTypes.InvokeResponse
});
}
}, true);
return this._app;
}
/**
* Registers a handler that implements the logic to handle the tap actions for items returned
* by a Search based message extension.
* @remarks
* The `composeExtension/selectItem` INVOKE activity does not contain any sort of command ID,
* so only a single select item handler can be registered. Developers will need to include a
* type name of some sort in the preview item they return if they need to support multiple
* select item handlers.
* @template TItem Optional. Type of the item being selected.
* @param {(context: TurnContext, state: TState, item: TItem) => Promise<MessagingExtensionResult>} handler Function to call when the command is received.
* @param {TurnContext} handler.context Context for the current turn of conversation with the user.
* @param {TState} handler.state Current state of the turn.
* @param {TItem} handler.item The item that was selected.
* @returns {Application<TState>} The application for chaining purposes.
*/
selectItem(handler) {
const { SELECT_ITEM_INVOKE } = MessageExtensionsInvokeNames;
// Define static route selector
const selector = (context) => Promise.resolve(context?.activity?.type == botbuilder_1.ActivityTypes.Invoke && context?.activity.name === SELECT_ITEM_INVOKE);
// Add route
this._app.addRoute(selector, async (context, state) => {
// Call handler and then check to see if an invoke response has already been added
const result = await handler(context, state, context?.activity?.value ?? {});
if (!context.turnState.get(botbuilder_1.INVOKE_RESPONSE_KEY)) {
// Format invoke response
const response = {
composeExtension: result
};
// Queue up invoke response
await context.sendActivity({
value: { body: response, status: 200 },
type: botbuilder_1.ActivityTypes.InvokeResponse
});
}
}, true);
return this._app;
}
/**
* Registers a handler that implements the submit action for an Action based Message Extension.
* @template TData Optional. Type of data being submitted.
* @param {string | RegExp | RouteSelector | string[] | RegExp[] | RouteSelector[]} commandId ID of the command(s) to register the handler for.
* @param {(context: TurnContext, state: TState, data: TData) => Promise<MessagingExtensionResult | TaskModuleTaskInfo | string | null | undefined>} handler Function to call when the command is received.
* @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 The data that was submitted.
* @returns {Application<TState>} The application for chaining purposes.
*/
submitAction(commandId, handler) {
const { SUBMIT_ACTION_INVOKE } = MessageExtensionsInvokeNames;
(Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => {
const selector = createTaskSelector(cid, SUBMIT_ACTION_INVOKE);
this._app.addRoute(selector, async (context, state) => {
// Insure that we're in an invoke as expected
if (context?.activity?.type !== botbuilder_1.ActivityTypes.Invoke ||
context?.activity?.name !== SUBMIT_ACTION_INVOKE) {
throw new Error(`Unexpected MessageExtensions.submitAction() triggered for activity type: ${context?.activity?.type}`);
}
// Call handler and then check to see if an invoke response has already been added
const result = await handler(context, state, context.activity.value?.data ?? {});
await this.returnSubmitActionResponse(context, result);
}, true);
});
return this._app;
}
/**
* Sends the response for a submit action.
* @param {TurnContext} context The context object for the current turn of conversation with the user.
* @param {MessagingExtensionResult | TaskModuleTaskInfo | string | null | undefined} result The result of the submit action.
* @private
*/
async returnSubmitActionResponse(context, result) {
if (!context.turnState.get(botbuilder_1.INVOKE_RESPONSE_KEY)) {
// Format invoke response
let response;
if (typeof result == 'string') {
// Return message
response = {
task: {
type: 'message',
value: result
}
};
}
else if (typeof result == 'object' && result != null) {
if (result.card) {
// Return another task module
response = {
task: {
type: 'continue',
value: result
}
};
}
else {
// Return card to user
response = {
composeExtension: result
};
}
}
else {
// No action taken
response = {
composeExtension: undefined
};
}
// Queue up invoke response
await context.sendActivity({
value: { body: response, status: 200 },
type: botbuilder_1.ActivityTypes.InvokeResponse
});
}
}
/**
* Registers a handler that invokes the fetch of the configuration settings for a Message Extension.
@remarks
* The `composeExtension/querySettingUrl` INVOKE activity does not contain a command ID, so only a single select item handler can be registered.
* @param {(context: TurnContext, state: TState) => Promise<MessagingExtensionResult>} handler Function defined by the developer to call when the command is received.
* @param {TurnContext} handler.context Context for the current turn of conversation with the user.
* @param {TState} handler.state Current state of the turn.
* @returns {Application<TState>} The application for chaining purposes.
*/
queryUrlSetting(handler) {
const { QUERY_SETTING_URL } = MessageExtensionsInvokeNames;
// Define static route selector
const selector = (context) => Promise.resolve(context?.activity?.type == botbuilder_1.ActivityTypes.Invoke && context?.activity.name === QUERY_SETTING_URL);
// Add route
this._app.addRoute(selector, async (context, state) => {
// Call handler and then check to see if an invoke response has already been added
const result = await handler(context, state);
if (!context.turnState.get(botbuilder_1.INVOKE_RESPONSE_KEY)) {
const response = {
composeExtension: result
};
await context.sendActivity({
value: { status: 200, body: response },
type: botbuilder_1.ActivityTypes.InvokeResponse
});
}
}, true);
return this._app;
}
/**
* Registers a handler that implements the logic to invoke configuring Message Extension settings
* @remarks
* The `composeExtension/setting` INVOKE activity does not contain a command ID, so only a single select item handler can be registered.
* @template TData Message Extension settings to be configured.
* @param {(context: TurnContext, state: TState, settings: TData) => Promise<void>} handler Function defined by the developer to call when the command is received.
* @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.settings The configuration settings that was submitted.
* @returns {Application<TState>} The application for chaining purposes.
*/
configureSettings(handler) {
const { CONFIGURE_SETTINGS } = MessageExtensionsInvokeNames;
// Define static route selector
const selector = (context) => Promise.resolve(context?.activity?.type == botbuilder_1.ActivityTypes.Invoke && context?.activity.name === CONFIGURE_SETTINGS);
// Add route
this._app.addRoute(selector, async (context, state) => {
// Call handler and then check to see if an invoke response has already been added
await handler(context, state, context.activity.value ?? {});
if (!context.turnState.get(botbuilder_1.INVOKE_RESPONSE_KEY)) {
// Queue up 'empty' invoke response with no body, as only a 200 status code is expected
await context.sendActivity({
value: { status: 200 },
type: botbuilder_1.ActivityTypes.InvokeResponse
});
}
}, true);
return this._app;
}
/**
* Registers a handler that implements the logic when a user has clicked on a button in a Message Extension card.
* @remarks
* The `composeExtension/onCardButtonClicked` INVOKE activity does not contain any sort of command ID,
* so only a single select item handler can be registered. Developers will need to include a
* type name of some sort in the preview item they return if they need to support multiple select item handlers.
* @template TData Message Extension data passed on invoke.
* @param {(context: TurnContext, state: TState, data: TData) => Promise<void>} handler Function defined by the developer to call when the command is received.
* @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 The data that was submitted.
* @returns {Application<TState>} The application for chaining purposes.
*/
handleOnButtonClicked(handler) {
const { QUERY_CARD_BUTTON_CLICKED } = MessageExtensionsInvokeNames;
// Define static route selector
const selector = (context) => Promise.resolve(context?.activity?.type == botbuilder_1.ActivityTypes.Invoke && context?.activity.name === QUERY_CARD_BUTTON_CLICKED);
// Add route
this._app.addRoute(selector, async (context, state) => {
// Call handler and then check to see if an invoke response has already been added
await handler(context, state, context.activity.value ?? {});
if (!context.turnState.get(botbuilder_1.INVOKE_RESPONSE_KEY)) {
// Queue up 'empty' invoke response with no body, as only a 200 status code is expected
await context.sendActivity({
value: { status: 200 },
type: botbuilder_1.ActivityTypes.InvokeResponse
});
}
}, true);
return this._app;
}
}
exports.MessageExtensions = MessageExtensions;
/**
* @private
* Creates a route selector function for a task module command.
* @param {string | RegExp | RouteSelector} commandId The ID of the command to register the handler for.
* @param {string} invokeName The name of the invoke activity.
* @param {'edit' | 'send'} botMessagePreviewAction The bot message preview action to match.
* @returns {RouteSelector} The route selector function.
*/
function createTaskSelector(commandId, invokeName, botMessagePreviewAction) {
if (typeof commandId == 'function') {
// Return the passed in selector function
return commandId;
}
else if (commandId instanceof RegExp) {
// Return a function that matches the commandId using a RegExp
return (context) => {
const isInvoke = context?.activity?.type == botbuilder_1.ActivityTypes.Invoke && context?.activity?.name == invokeName;
if (isInvoke &&
typeof context?.activity?.value?.commandId == 'string' &&
matchesPreviewAction(context.activity, botMessagePreviewAction)) {
return Promise.resolve(commandId.test(context.activity.value.commandId));
}
else {
return Promise.resolve(false);
}
};
}
else {
// Return a function that attempts to match commandId
return (context) => {
const isInvoke = context?.activity?.type == botbuilder_1.ActivityTypes.Invoke && context?.activity?.name == invokeName;
return Promise.resolve(isInvoke &&
context?.activity?.value?.commandId === commandId &&
matchesPreviewAction(context.activity, botMessagePreviewAction));
};
}
}
/**
* @private
* Checks if the bot message preview action matches the specified action.
* @param {Activity} activity The activity to check.
* @param {'edit' | 'send'} botMessagePreviewAction The bot message preview action to match.
* @returns {boolean} True if the bot message preview action matches, false otherwise.
*/
function matchesPreviewAction(activity, botMessagePreviewAction) {
if (typeof activity?.value?.botMessagePreviewAction == 'string') {
return activity.value.botMessagePreviewAction == botMessagePreviewAction;
}
else {
return botMessagePreviewAction == undefined;
}
}
//# sourceMappingURL=MessageExtensions.js.map