UNPKG

@microsoft/agents-hosting-teams

Version:

Microsoft 365 Agents SDK for JavaScript

292 lines (272 loc) 11.7 kB
/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { Activity, ActivityTypes } from '@microsoft/agents-activity' import { AdaptiveCardInvokeResponse, CardFactory, INVOKE_RESPONSE_KEY, InvokeResponse, MessageFactory, RouteSelector, TurnContext, TurnState } from '@microsoft/agents-hosting' import { AdaptiveCard } from './adaptiveCard' import { TeamsApplication } from '../teamsApplication' import { AdaptiveCardActionExecuteResponseType } from './adaptiveCardActionExecuteResponseType' import { AdaptiveCardInvokeResponseType } from './adaptiveCardInvokeResponseType' import { AdaptiveCardSearchResult } from './adaptiveCardSearchResult' import { parseAdaptiveCardInvokeAction, parseValueActionExecuteSelector, parseValueDataset, parseValueSearchQuery } from '../../parsers' import { Query } from '../query' import { AdaptiveCardsSearchParams } from '../../adaptive-cards' export const ACTION_INVOKE_NAME = 'adaptiveCard/action' const ACTION_EXECUTE_TYPE = 'Action.Execute' const DEFAULT_ACTION_SUBMIT_FILTER = 'verb' const SEARCH_INVOKE_NAME = 'application/search' /** * A class to handle Adaptive Card actions such as executing actions, submitting actions, and performing searches. * @template TState - The type of the TurnState used in the application. */ export class AdaptiveCardsActions<TState extends TurnState> { /** * The Teams application instance associated with this class. */ private readonly _app: TeamsApplication<TState> /** * Constructs an instance of AdaptiveCardsActions. * @param app - The Teams application instance. */ public constructor (app: TeamsApplication<TState>) { this._app = app } /** * Registers a handler for the Action.Execute event. * @template TData - The type of the data passed to the handler. * @param verb - A string, RegExp, RouteSelector, or an array of these to match the action verb. * @param handler - A function to handle the action execution. * @returns The Teams application instance. */ public actionExecute<TData = Record<string, any>>( verb: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[], handler: (context: TurnContext, state: TState, data: TData) => Promise<AdaptiveCard | string> ): TeamsApplication<TState> { let actionExecuteResponseType = this._app.teamsOptions.adaptiveCards?.actionExecuteResponseType ?? AdaptiveCardActionExecuteResponseType.REPLACE_FOR_INTERACTOR; (Array.isArray(verb) ? verb : [verb]).forEach((v) => { const selector = createActionExecuteSelector(v) this._app.addRoute( selector, async (context, state) => { const a = context?.activity const invokeAction = parseValueActionExecuteSelector(a.value) if ( a?.type !== ActivityTypes.Invoke || a?.name !== ACTION_INVOKE_NAME || (invokeAction.action.type !== ACTION_EXECUTE_TYPE) ) { throw new Error( `Unexpected AdaptiveCards.actionExecute() triggered for activity type: ${invokeAction.action.type}` ) } const result = await handler(context, state, (invokeAction.action as TData) ?? {} as TData) if (!context.turnState.get(INVOKE_RESPONSE_KEY)) { let response: AdaptiveCardInvokeResponse if (typeof result === 'string') { response = { statusCode: 200, type: AdaptiveCardInvokeResponseType.MESSAGE, value: result as any } await sendInvokeResponse(context, response) } else { if ( result.refresh && actionExecuteResponseType !== AdaptiveCardActionExecuteResponseType.NEW_MESSAGE_FOR_ALL ) { actionExecuteResponseType = AdaptiveCardActionExecuteResponseType.REPLACE_FOR_ALL } const activity = MessageFactory.attachment(CardFactory.adaptiveCard(result)) response = { statusCode: 200, type: AdaptiveCardInvokeResponseType.ADAPTIVE, value: result } if ( actionExecuteResponseType === AdaptiveCardActionExecuteResponseType.NEW_MESSAGE_FOR_ALL ) { await sendInvokeResponse(context, { statusCode: 200, type: AdaptiveCardInvokeResponseType.MESSAGE, value: 'Your response was sent to the app' as any }) await context.sendActivity(activity) } else if ( actionExecuteResponseType === AdaptiveCardActionExecuteResponseType.REPLACE_FOR_ALL ) { activity.id = context.activity.replyToId await context.updateActivity(activity) await sendInvokeResponse(context, response) } else { await sendInvokeResponse(context, response) } } } }, true ) }) return this._app } /** * Registers a handler for the Action.Submit event. * @template TData - The type of the data passed to the handler. * @param verb - A string, RegExp, RouteSelector, or an array of these to match the action verb. * @param handler - A function to handle the action submission. * @returns The Teams application instance. */ public actionSubmit<TData = Record<string, any>>( verb: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[], handler: (context: TurnContext, state: TState, data: TData) => Promise<void> ): TeamsApplication<TState> { const filter = this._app.teamsOptions.adaptiveCards?.actionSubmitFilter ?? DEFAULT_ACTION_SUBMIT_FILTER; (Array.isArray(verb) ? verb : [verb]).forEach((v) => { const selector = createActionSubmitSelector(v, filter) this._app.addRoute(selector, async (context, state) => { const a = context?.activity if (a?.type !== ActivityTypes.Message || a?.text || typeof a?.value !== 'object') { throw new Error(`Unexpected AdaptiveCards.actionSubmit() triggered for activity type: ${a?.type}`) } await handler(context, state as TState, (parseAdaptiveCardInvokeAction(a.value)).data as TData ?? {} as TData) }) }) return this._app } /** * Registers a handler for the search event. * @param dataset - A string, RegExp, RouteSelector, or an array of these to match the dataset. * @param handler - A function to handle the search query. * @returns The Teams application instance. */ public search ( dataset: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[], handler: ( context: TurnContext, state: TState, query: Query<AdaptiveCardsSearchParams> ) => Promise<AdaptiveCardSearchResult[]> ): TeamsApplication<TState> { (Array.isArray(dataset) ? dataset : [dataset]).forEach((ds) => { const selector = createSearchSelector(ds) this._app.addRoute( selector, async (context, state) => { const a = context?.activity if (a?.type !== ActivityTypes.Invoke || a?.name !== SEARCH_INVOKE_NAME) { throw new Error(`Unexpected AdaptiveCards.search() triggered for activity type: ${a?.type}`) } const parsedQuery = parseValueSearchQuery(a.value) const query: Query<AdaptiveCardsSearchParams> = { count: parsedQuery.queryOptions?.top ?? 25, skip: parsedQuery.queryOptions?.skip ?? 0, parameters: { queryText: parsedQuery.queryText ?? '', dataset: parsedQuery.dataset ?? '' } } const results = await handler(context, state, query) if (!context.turnState.get(INVOKE_RESPONSE_KEY)) { const response = { type: AdaptiveCardInvokeResponseType.SEARCH, value: { results } } await context.sendActivity({ value: { body: response, status: 200 } as InvokeResponse, type: ActivityTypes.InvokeResponse } as Activity) } }, true ) }) return this._app } } function createActionExecuteSelector (verb: string | RegExp | RouteSelector): RouteSelector { if (typeof verb === 'function') { return verb } else if (verb instanceof RegExp) { return (context: TurnContext) => { const a = context?.activity const valueAction = parseValueActionExecuteSelector(a.value) const isInvoke = a?.type === ActivityTypes.Invoke && a?.name === ACTION_INVOKE_NAME && valueAction.action?.type === ACTION_EXECUTE_TYPE if (isInvoke && typeof valueAction.action.verb === 'string') { return Promise.resolve(verb.test(valueAction.action.verb)) } else { return Promise.resolve(false) } } } else { return (context: TurnContext) => { const a = context?.activity const valueAction = parseValueActionExecuteSelector(a.value) const isInvoke = a?.type === ActivityTypes.Invoke && a?.name === ACTION_INVOKE_NAME && valueAction.action?.type === ACTION_EXECUTE_TYPE if (isInvoke && valueAction.action?.verb === verb) { return Promise.resolve(true) } else { return Promise.resolve(false) } } } } function createActionSubmitSelector (verb: string | RegExp | RouteSelector, filter: string): RouteSelector { if (typeof verb === 'function') { return verb } else if (verb instanceof RegExp) { return (context: TurnContext) => { const a = context?.activity const isSubmit = a?.type === ActivityTypes.Message && !a?.text && typeof a?.value === 'object' if (isSubmit && typeof (a?.value as any)[filter] === 'string') { return Promise.resolve(verb.test((a.value as any)[filter])) } else { return Promise.resolve(false) } } } else { return (context: TurnContext) => { const a = context?.activity const isSubmit = a?.type === ActivityTypes.Message && !a?.text && typeof a?.value === 'object' return Promise.resolve(isSubmit && (a?.value as any)[filter] === verb) } } } function createSearchSelector (dataset: string | RegExp | RouteSelector): RouteSelector { if (typeof dataset === 'function') { return dataset } else if (dataset instanceof RegExp) { return (context: TurnContext) => { const a = context?.activity const valueDataset = parseValueDataset(a.value) const isSearch = a?.type === ActivityTypes.Invoke && a?.name === SEARCH_INVOKE_NAME if (isSearch && typeof valueDataset.dataset === 'string') { return Promise.resolve(dataset.test(valueDataset.dataset)) } else { return Promise.resolve(false) } } } else { return (context: TurnContext) => { const a = context?.activity const valueDataset = parseValueDataset(a.value) const isSearch = a?.type === ActivityTypes.Invoke && a?.name === SEARCH_INVOKE_NAME return Promise.resolve(isSearch && valueDataset.dataset === dataset) } } } async function sendInvokeResponse (context: TurnContext, response: AdaptiveCardInvokeResponse) { await context.sendActivity({ value: { body: response, status: 200 } as InvokeResponse, type: ActivityTypes.InvokeResponse } as Activity) }