@microsoft/agents-hosting-extensions-teams
Version:
Microsoft 365 Agents SDK for JavaScript. Teams extensions
242 lines (227 loc) • 9.79 kB
text/typescript
import { Activity, ActivityTypes, Channels } from '@microsoft/agents-activity'
import { AgentApplication, INVOKE_RESPONSE_KEY, InvokeResponse, RouteHandler, RouteSelector, TurnContext, TurnState } from '@microsoft/agents-hosting'
import { TaskModuleTaskInfo } from './taskModuleTaskInfo'
import { TaskModuleResponse } from './taskModuleResponse'
export enum TaskModuleInvokeNames {
CONFIG_FETCH_INVOKE_NAME = 'config/fetch',
CONFIG_SUBMIT_INVOKE_NAME = 'config/submit',
FETCH_INVOKE_NAME = 'task/fetch',
SUBMIT_INVOKE_NAME = 'task/submit',
DEFAULT_TASK_DATA_FILTER = 'verb'
}
interface TaskModuleOptions {
taskDataFilter?: string
}
/**
* Class that exposes Teams task module-related events.
* Provides an organized way to handle task module operations in Microsoft Teams.
*/
export class TaskModule<TState extends TurnState> {
_app: AgentApplication<TState>
_options: TaskModuleOptions
/**
* Creates a new instance of the TaskModule class.
* @param app - The agent application
*/
constructor (app: AgentApplication<TState>, options?: TaskModuleOptions) {
this._options = options ?? { taskDataFilter: TaskModuleInvokeNames.DEFAULT_TASK_DATA_FILTER }
this._app = app
}
/**
* Handles task module fetch events. These occur when a task module is requested to be displayed.
* @param handler - The handler to call when a task module fetch event occurs
* @returns this (for method chaining)
*/
onFetch (handler: RouteHandler<TurnState>) {
const routeSel: RouteSelector = (context: TurnContext) => {
return Promise.resolve(
context.activity.type === ActivityTypes.Invoke &&
context.activity.channelId === 'msteams' &&
context.activity.name === 'task/fetch'
)
}
this._app.addRoute(routeSel, handler, true) // Invoke requires true
return this
}
/**
* Registers a handler to process the submission of a task module.
* @remarks
* Handlers should respond with another TaskInfo object, message string, or `null` to indicate
* the task is completed.
* @template TData Optional. Type of the data object being passed to the handler.
* @param {string | RegExp | RouteSelector | string[] | RegExp[] | RouteSelector[]} verb - Name of the verb(s) to register the handler for.
* @param {(context: TurnContext, state: TState, data: TData) => Promise<TaskModuleTaskInfo | string | null | undefined>} 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 submit<TData extends Record<string, any> = Record<string, any>>(
verb: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[],
handler: (
context: TurnContext,
state: TState,
data: TData
) => Promise<TaskModuleTaskInfo | string | null | undefined>
): AgentApplication<TState> {
(Array.isArray(verb) ? verb : [verb]).forEach((v) => {
const { DEFAULT_TASK_DATA_FILTER, SUBMIT_INVOKE_NAME } = TaskModuleInvokeNames
const filterField = this._options.taskDataFilter ?? DEFAULT_TASK_DATA_FILTER
const selector = createTaskSelector(v, filterField, SUBMIT_INVOKE_NAME)
this._app.addRoute(
selector,
async (context, state) => {
if (context?.activity?.channelId === Channels.Msteams) {
if (context?.activity?.type !== ActivityTypes.Invoke || context?.activity?.name !== SUBMIT_INVOKE_NAME) {
throw new Error(`Unexpected TaskModules.submit() triggered for activity type: ${context?.activity?.type}`
)
}
const result = await handler(context, state, (context.activity.value as any).data ?? {})
if (!result) {
await context.sendActivity(Activity.fromObject({
value: { status: 200 } as InvokeResponse,
type: ActivityTypes.InvokeResponse
}))
}
if (!context.turnState.get(INVOKE_RESPONSE_KEY)) {
let response: TaskModuleResponse | undefined
if (typeof result === 'string') {
response = {
task: {
type: 'message',
value: result
}
}
} else if (typeof result === 'object') {
// Return card
response = {
task: {
type: 'continue',
value: result as TaskModuleTaskInfo
}
}
}
// Queue up invoke response
await context.sendActivity(Activity.fromObject({
value: { body: response, status: 200 } as InvokeResponse,
type: ActivityTypes.InvokeResponse
}))
}
}
},
true
)
})
return this._app
}
/**
* Handles specific task module fetch events based on a verb/action.
* @param verb - The verb or action identifier to match against in the task module data
* @param handler - The handler to call when a matching task module fetch event occurs
* @returns this (for method chaining)
*/
onFetchByVerb (verb: string, handler: RouteHandler<TurnState>) {
const routeSel: RouteSelector = (context: TurnContext) => {
return Promise.resolve(
context.activity.type === ActivityTypes.Invoke &&
context.activity.channelId === 'msteams' &&
context.activity.name === 'task/fetch' &&
// @ts-ignore
context.activity.value?.data === verb
)
}
this._app.addRoute(routeSel, handler, true)
return this
}
/**
* Handles specific task module submit events based on a verb/action.
* @param verb - The verb or action identifier to match against in the task module data
* @param handler - The handler to call when a matching task module submit event occurs
* @returns this (for method chaining)
*/
onSubmitByVerb (verb: string, handler: RouteHandler<TurnState>) {
const routeSel: RouteSelector = (context: TurnContext) => {
return Promise.resolve(
context.activity.type === ActivityTypes.Invoke &&
context.activity.channelId === 'msteams' &&
context.activity.name === 'task/submit' &&
// @ts-ignore
context.activity.value?.data === verb
)
}
this._app.addRoute(routeSel, handler, true)
return this
}
/**
* Handles configuration fetch events. These occur when an agent configuration is requested.
* @param handler - The handler to call when a configuration fetch event occurs
* @returns this (for method chaining)
*/
onConfigurationFetch (handler: RouteHandler<TurnState>) {
const routeSel: RouteSelector = (context: TurnContext) => {
return Promise.resolve(
context.activity.type === ActivityTypes.Invoke &&
context.activity.channelId === 'msteams' &&
context.activity.name === 'config/fetch'
)
}
this._app.addRoute(routeSel, handler, true)
return this
}
/**
* Handles configuration submit events. These occur when an agent configuration is submitted.
* @param handler - The handler to call when a configuration submit event occurs
* @returns this (for method chaining)
*/
onConfigurationSubmit (handler: RouteHandler<TurnState>) {
const routeSel: RouteSelector = (context: TurnContext) => {
return Promise.resolve(
context.activity.type === ActivityTypes.Invoke &&
context.activity.channelId === 'msteams' &&
context.activity.name === 'config/submit'
)
}
this._app.addRoute(routeSel, handler, true)
return this
}
}
/**
* Creates a route selector function for a given verb, filter field, and invoke name.
* @param {string | RegExp | RouteSelector} verb - The verb to match.
* @param {string} filterField - The field to use for filtering.
* @param {string} invokeName - The name of the invoke action.
* @returns {RouteSelector} The route selector function.
* @private
* @remarks
* This function is used to create a route selector function for a given verb, filter field, and invoke name.
* The route selector function is used to match incoming requests to the appropriate handler function.
*/
function createTaskSelector (
verb: string | RegExp | RouteSelector,
filterField: string,
invokeName: string
): RouteSelector {
if (typeof verb === 'function') {
// Return the passed in selector function
return verb
} else if (verb instanceof RegExp) {
// Return a function that matches the verb using a RegExp
return (context: TurnContext) => {
const isTeams = context.activity.channelId === Channels.Msteams
const isInvoke = context?.activity?.type === ActivityTypes.Invoke && context?.activity?.name === invokeName
const data = (context?.activity?.value as any).data
if (isInvoke && isTeams && typeof data === 'object' && typeof data[filterField] === 'string') {
return Promise.resolve(verb.test(data[filterField]))
} else {
return Promise.resolve(false)
}
}
} else {
// Return a function that attempts to match verb
return (context: TurnContext) => {
const isInvoke = context?.activity?.type === ActivityTypes.Invoke && context?.activity?.name === invokeName
const data = (context?.activity?.value as any).data
return Promise.resolve(isInvoke && typeof data === 'object' && data[filterField] === verb)
}
}
}