UNPKG

@microsoft/agents-hosting

Version:

Microsoft 365 Agents SDK for JavaScript

671 lines 27.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ActivityHandler = exports.INVOKE_RESPONSE_KEY = void 0; /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ const logger_1 = require("@microsoft/agents-activity/logger"); const agents_activity_1 = require("@microsoft/agents-activity"); const statusCodes_1 = require("./statusCodes"); const invokeException_1 = require("./invoke/invokeException"); const tokenResponseEventName_1 = require("./tokenResponseEventName"); /** Symbol key for invoke response */ exports.INVOKE_RESPONSE_KEY = Symbol('invokeResponse'); const logger = (0, logger_1.debug)('agents:activity-handler'); /** * Handles incoming activities from channels and dispatches them to the appropriate handlers. * * @remarks * This class is provided to simplify the migration from Bot Framework SDK v4 to the Agents Hosting framework. * * The ActivityHandler serves as the central hub for processing incoming activities in conversational AI applications. * It provides a comprehensive framework for handling various activity types including messages, conversation updates, * message reactions, typing indicators, installation updates, and invoke operations such as adaptive cards and search. * * ## Key Features: * - **Activity Routing**: Automatically routes activities to appropriate handlers based on activity type * - **Handler Registration**: Provides fluent API methods (onMessage, onConversationUpdate, etc.) for registering event handlers * - **Invoke Support**: Built-in handling for adaptive card actions and search invoke operations * - **Error Handling**: Robust error handling with proper HTTP status codes for invoke operations * - **Extensibility**: Designed for inheritance to allow custom behavior and specialized handlers * * ## Usage: * ```typescript * const handler = new ActivityHandler() * .onMessage(async (context, next) => { * await context.sendActivity('Hello!'); * await next(); * }) * .onMembersAdded(async (context, next) => { * // Welcome new members * await next(); * }); * ``` * * Developers can extend this class to implement domain-specific logic, override default behaviors, * or add support for custom activity types and invoke operations. */ class ActivityHandler { constructor() { /** * Collection of handlers registered for different activity types * @protected */ this.handlers = {}; } /** * Registers a handler for the Turn activity type. * This is called for all activities regardless of type. * @param handler - The handler to register * @returns The current instance for method chaining */ onTurn(handler) { return this.on('Turn', handler); } /** * Registers a handler for the MembersAdded activity type. * This is called when new members are added to the conversation. * @param handler - The handler to register * @returns The current instance for method chaining */ onMembersAdded(handler) { return this.on('MembersAdded', handler); } /** * Registers a handler for the MembersRemoved activity type. * This is called when members are removed from the conversation. * @param handler - The handler to register * @returns The current instance for method chaining */ onMembersRemoved(handler) { return this.on('MembersRemoved', handler); } /** * Registers a handler for the Message activity type. * This is called when a message is received from the user. * @param handler - The handler to register * @returns The current instance for method chaining */ onMessage(handler) { return this.on('Message', handler); } /** * Registers a handler for the MessageUpdate activity type. * This is called when a message is updated. * @param handler - The handler to register * @returns The current instance for method chaining */ onMessageUpdate(handler) { return this.on('MessageUpdate', handler); } /** * Registers a handler for the MessageDelete activity type. * This is called when a message is deleted. * @param handler - The handler to register * @returns The current instance for method chaining */ onMessageDelete(handler) { return this.on('MessageDelete', handler); } /** * Registers a handler for the ConversationUpdate activity type. * This is called when the conversation is updated, such as when members are added or removed. * @param handler - The handler to register * @returns The current instance for method chaining */ onConversationUpdate(handler) { return this.on('ConversationUpdate', handler); } /** * Registers a handler for the MessageReaction activity type. * This is called when reactions are added or removed from messages. * @param handler - The handler to register * @returns The current instance for method chaining */ onMessageReaction(handler) { return this.on('MessageReaction', handler); } /** * Registers a handler for the ReactionsAdded activity type. * This is called when reactions are added to messages. * @param handler - The handler to register * @returns The current instance for method chaining */ onReactionsAdded(handler) { return this.on('ReactionsAdded', handler); } /** * Registers a handler for the ReactionsRemoved activity type. * This is called when reactions are removed from messages. * @param handler - The handler to register * @returns The current instance for method chaining */ onReactionsRemoved(handler) { return this.on('ReactionsRemoved', handler); } /** * Registers a handler for the Typing activity type. * This is called when a typing indicator is received. * @param handler - The handler to register * @returns The current instance for method chaining */ onTyping(handler) { return this.on('Typing', handler); } /** * Registers a handler for the InstallationUpdate activity type. * This is called when an agent is installed or uninstalled. * @param handler - The handler to register * @returns The current instance for method chaining */ onInstallationUpdate(handler) { return this.on('InstallationUpdate', handler); } /** * Registers a handler for the InstallationUpdateAdd activity type. * This is called when an agent is installed or upgraded. * @param handler - The handler to register * @returns The current instance for method chaining */ onInstallationUpdateAdd(handler) { return this.on('InstallationUpdateAdd', handler); } /** * Registers a handler for the InstallationUpdateRemove activity type. * This is called when an agent is uninstalled or downgraded. * @param handler - The handler to register * @returns The current instance for method chaining */ onInstallationUpdateRemove(handler) { return this.on('InstallationUpdateRemove', handler); } /** * Registers a handler for the EndOfConversation activity type. * This is called when the conversation ends. * @param handler - The handler to register * @returns The current instance for method chaining */ onEndOfConversation(handler) { return this.on('EndOfConversation', handler); } /** * Registers a handler for the SignInInvoke activity type. * This is called when a sign-in is requested. * @param handler - The handler to register * @returns The current instance for method chaining */ onSignInInvoke(handler) { return this.on('SignInInvoke', handler); } /** * Registers a handler for unrecognized activity types. * This is called when an activity type is not recognized. * @param handler - The handler to register * @returns The current instance for method chaining */ onUnrecognizedActivityType(handler) { return this.on('UnrecognizedActivityType', handler); } /** * Registers an activity event handler for the _dialog_ event, emitted as the last event for an incoming activity. * This handler is called after all other handlers have been processed. * @param handler - The handler to register * @returns The current instance for method chaining */ onDialog(handler) { return this.on('Default', handler); } /** * Runs the activity handler pipeline. * This method is called to process an incoming activity through the registered handlers. * @param context - The turn context for the current turn of conversation * @throws Error if context is missing, activity is missing, or activity type is missing */ async run(context) { if (!context) throw new Error('Missing TurnContext parameter'); if (!context.activity) throw new Error('TurnContext does not include an activity'); if (!context.activity.type) throw new Error('Activity is missing its type'); await this.onTurnActivity(context); } /** * Handles the Turn activity. * This method is called for every activity type and dispatches to the appropriate handler. * @param context - The turn context for the current turn of conversation * @protected */ async onTurnActivity(context) { switch (context.activity.type) { case agents_activity_1.ActivityTypes.Message: await this.onMessageActivity(context); break; case agents_activity_1.ActivityTypes.MessageUpdate: await this.onMessageUpdateActivity(context); break; case agents_activity_1.ActivityTypes.MessageDelete: await this.onMessageDeleteActivity(context); break; case agents_activity_1.ActivityTypes.ConversationUpdate: await this.onConversationUpdateActivity(context); break; case agents_activity_1.ActivityTypes.Invoke: { const invokeResponse = await this.onInvokeActivity(context); if (invokeResponse && !context.turnState.get(exports.INVOKE_RESPONSE_KEY)) { const activity = agents_activity_1.Activity.fromObject({ value: invokeResponse, type: 'invokeResponse' }); await context.sendActivity(activity); } break; } case agents_activity_1.ActivityTypes.MessageReaction: await this.onMessageReactionActivity(context); break; case agents_activity_1.ActivityTypes.Typing: await this.onTypingActivity(context); break; case agents_activity_1.ActivityTypes.InstallationUpdate: await this.onInstallationUpdateActivity(context); break; case agents_activity_1.ActivityTypes.EndOfConversation: await this.onEndOfConversationActivity(context); break; default: await this.onUnrecognizedActivity(context); break; } } /** * Handles the Message activity. * This method processes incoming message activities. * @param context - The turn context for the current turn of conversation * @protected */ async onMessageActivity(context) { await this.handle(context, 'Message', this.defaultNextEvent(context)); } /** * Handles the MessageUpdate activity. * This method processes message update activities. * @param context - The turn context for the current turn of conversation * @protected */ async onMessageUpdateActivity(context) { await this.handle(context, 'MessageUpdate', async () => { await this.dispatchMessageUpdateActivity(context); }); } /** * Handles the MessageDelete activity. * This method processes message deletion activities. * @param context - The turn context for the current turn of conversation * @protected */ async onMessageDeleteActivity(context) { await this.handle(context, 'MessageDelete', async () => { await this.dispatchMessageDeleteActivity(context); }); } /** * Handles the ConversationUpdate activity. * This method processes conversation update activities. * @param context - The turn context for the current turn of conversation * @protected */ async onConversationUpdateActivity(context) { await this.handle(context, 'ConversationUpdate', async () => { await this.dispatchConversationUpdateActivity(context); }); } /** * Handles the SignInInvoke activity. * This method processes sign-in invoke activities. * @param context - The turn context for the current turn of conversation * @protected */ async onSigninInvokeActivity(context) { await this.handle(context, 'SignInInvoke', this.defaultNextEvent(context)); } /** * Handles the Invoke activity. * This method processes various invoke activities based on their name. * @param context - The turn context for the current turn of conversation * @returns An invoke response object with status and body * @protected */ async onInvokeActivity(context) { try { switch (context.activity.name) { case 'application/search': { const invokeValue = this.getSearchInvokeValue(context.activity); const response = await this.onSearchInvoke(context, invokeValue); return { status: response.statusCode, body: response }; } case 'adaptiveCard/action': { const invokeValue = this.getAdaptiveCardInvokeValue(context.activity); const response = await this.onAdaptiveCardInvoke(context, invokeValue); return { status: response.statusCode, body: response }; } case 'signin/verifyState': case 'signin/tokenExchange': await this.onSigninInvokeActivity(context); return { status: statusCodes_1.StatusCodes.OK }; default: throw new invokeException_1.InvokeException(statusCodes_1.StatusCodes.NOT_IMPLEMENTED); } } catch (err) { const error = err; if (error.message === 'NotImplemented') { return { status: statusCodes_1.StatusCodes.NOT_IMPLEMENTED }; } if (err instanceof invokeException_1.InvokeException) { return err.createInvokeResponse(); } throw err; } finally { this.defaultNextEvent(context)(); } } /** * Handles the AdaptiveCardInvoke activity. * This method processes adaptive card invoke activities. * @param _context - The turn context for the current turn of conversation * @param _invokeValue - The adaptive card invoke value * @returns A promise that resolves to an adaptive card invoke response * @protected */ async onAdaptiveCardInvoke(_context, _invokeValue) { return await Promise.reject(new invokeException_1.InvokeException(statusCodes_1.StatusCodes.NOT_IMPLEMENTED)); } /** * Handles the SearchInvoke activity. * This method processes search invoke activities. * @param _context - The turn context for the current turn of conversation * @param _invokeValue - The search invoke value * @returns A promise that resolves to a search invoke response * @protected */ async onSearchInvoke(_context, _invokeValue) { return await Promise.reject(new invokeException_1.InvokeException(statusCodes_1.StatusCodes.NOT_IMPLEMENTED)); } /** * Retrieves the SearchInvoke value from the activity. * This method extracts and validates the search invoke value from an activity. * @param activity - The activity to extract the search invoke value from * @returns The validated search invoke value * @private */ getSearchInvokeValue(activity) { const value = activity.value; if (!value) { const response = this.createAdaptiveCardInvokeErrorResponse(statusCodes_1.StatusCodes.BAD_REQUEST, 'BadRequest', 'Missing value property for search'); throw new invokeException_1.InvokeException(statusCodes_1.StatusCodes.BAD_REQUEST, response); } if (!value.kind) { if (activity.channelId === agents_activity_1.Channels.Msteams) { value.kind = 'search'; } else { const response = this.createAdaptiveCardInvokeErrorResponse(statusCodes_1.StatusCodes.BAD_REQUEST, 'BadRequest', 'Missing kind property for search.'); throw new invokeException_1.InvokeException(statusCodes_1.StatusCodes.BAD_REQUEST, response); } } if (!value.queryText) { const response = this.createAdaptiveCardInvokeErrorResponse(statusCodes_1.StatusCodes.BAD_REQUEST, 'BadRequest', 'Missing queryText for search.'); throw new invokeException_1.InvokeException(statusCodes_1.StatusCodes.BAD_REQUEST, response); } return value; } /** * Retrieves the AdaptiveCardInvoke value from the activity. * This method extracts and validates the adaptive card invoke value from an activity. * @param activity - The activity to extract the adaptive card invoke value from * @returns The validated adaptive card invoke value * @private */ getAdaptiveCardInvokeValue(activity) { const value = activity.value; if (!value) { const response = this.createAdaptiveCardInvokeErrorResponse(statusCodes_1.StatusCodes.BAD_REQUEST, 'BadRequest', 'Missing value property'); throw new invokeException_1.InvokeException(statusCodes_1.StatusCodes.BAD_REQUEST, response); } if (value.action.type !== 'Action.Execute') { const response = this.createAdaptiveCardInvokeErrorResponse(statusCodes_1.StatusCodes.BAD_REQUEST, 'NotSupported', `The action '${value.action.type}' is not supported.`); throw new invokeException_1.InvokeException(statusCodes_1.StatusCodes.BAD_REQUEST, response); } const { action, authentication, state } = value; const { data, id: actionId, type, verb } = action !== null && action !== void 0 ? action : {}; const { connectionName, id: authenticationId, token } = authentication !== null && authentication !== void 0 ? authentication : {}; return { action: { data, id: actionId, type, verb }, authentication: { connectionName, id: authenticationId, token }, state }; } /** * Creates an error response for AdaptiveCardInvoke. * This method creates an error response for adaptive card invoke activities. * @param statusCode - The HTTP status code for the response * @param code - The error code * @param message - The error message * @returns An adaptive card invoke error response * @private */ createAdaptiveCardInvokeErrorResponse(statusCode, code, message) { return { statusCode, type: 'application/vnd.microsoft.error', value: { code, message } }; } /** * Handles the MessageReaction activity. * This method processes message reaction activities. * @param context - The turn context for the current turn of conversation * @protected */ async onMessageReactionActivity(context) { await this.handle(context, 'MessageReaction', async () => { await this.dispatchMessageReactionActivity(context); }); } /** * Handles the EndOfConversation activity. * This method processes end of conversation activities. * @param context - The turn context for the current turn of conversation * @protected */ async onEndOfConversationActivity(context) { await this.handle(context, 'EndOfConversation', this.defaultNextEvent(context)); } /** * Handles the Typing activity. * This method processes typing indicator activities. * @param context - The turn context for the current turn of conversation * @protected */ async onTypingActivity(context) { await this.handle(context, 'Typing', this.defaultNextEvent(context)); } /** * Handles the InstallationUpdate activity. * This method processes installation update activities. * @param context - The turn context for the current turn of conversation * @protected */ async onInstallationUpdateActivity(context) { switch (context.activity.action) { case 'add': case 'add-upgrade': await this.handle(context, 'InstallationUpdateAdd', this.defaultNextEvent(context)); return; case 'remove': case 'remove-upgrade': await this.handle(context, 'InstallationUpdateRemove', this.defaultNextEvent(context)); } } /** * Handles unrecognized activity types. * This method processes activities with unrecognized types. * @param context - The turn context for the current turn of conversation * @protected */ async onUnrecognizedActivity(context) { await this.handle(context, 'UnrecognizedActivityType', this.defaultNextEvent(context)); } /** * Dispatches the ConversationUpdate activity. * This method dispatches conversation update activities to the appropriate handlers. * @param context - The turn context for the current turn of conversation * @protected */ async dispatchConversationUpdateActivity(context) { if ((context.activity.membersAdded != null) && context.activity.membersAdded.length > 0) { await this.handle(context, 'MembersAdded', this.defaultNextEvent(context)); } else if ((context.activity.membersRemoved != null) && context.activity.membersRemoved.length > 0) { await this.handle(context, 'MembersRemoved', this.defaultNextEvent(context)); } else { await this.defaultNextEvent(context)(); } } /** * Dispatches the MessageReaction activity. * This method dispatches message reaction activities to the appropriate handlers. * @param context - The turn context for the current turn of conversation * @protected */ async dispatchMessageReactionActivity(context) { var _a, _b; if ((context.activity.reactionsAdded != null) || (context.activity.reactionsRemoved != null)) { if ((_a = context.activity.reactionsAdded) === null || _a === void 0 ? void 0 : _a.length) { await this.handle(context, 'ReactionsAdded', this.defaultNextEvent(context)); } if ((_b = context.activity.reactionsRemoved) === null || _b === void 0 ? void 0 : _b.length) { await this.handle(context, 'ReactionsRemoved', this.defaultNextEvent(context)); } } else { await this.defaultNextEvent(context)(); } } /** * Dispatches the MessageUpdate activity. * This method dispatches message update activities to the appropriate handlers. * @param context - The turn context for the current turn of conversation * @protected */ async dispatchMessageUpdateActivity(context) { await this.defaultNextEvent(context)(); } /** * Dispatches the MessageDelete activity. * This method dispatches message delete activities to the appropriate handlers. * @param context - The turn context for the current turn of conversation * @protected */ async dispatchMessageDeleteActivity(context) { await this.defaultNextEvent(context)(); } /** * Returns the default next event handler. * This method creates a function that calls the default handler. * @param context - The turn context for the current turn of conversation * @returns A function that calls the default handler * @protected */ defaultNextEvent(context) { const defaultHandler = async () => { await this.handle(context, 'Default', async () => { // noop }); }; return defaultHandler; } /** * Registers a handler for a specific activity type. * This method adds a handler to the list of handlers for a specific activity type. * @param type - The activity type to register the handler for * @param handler - The handler to register * @returns The current instance for method chaining * @protected */ on(type, handler) { if (!this.handlers[type]) { this.handlers[type] = [handler]; } else { this.handlers[type].push(handler); } return this; } /** * Executes the handlers for a specific activity type. * This method calls each registered handler for the specified activity type. * @param context - The turn context for the current turn of conversation * @param type - The activity type to handle * @param onNext - The function to call when all handlers have been executed * @returns The value returned by the last handler * @protected */ async handle(context, type, onNext) { let returnValue = null; async function runHandler(index) { if (index < handlers.length) { const val = await handlers[index](context, async () => await runHandler(index + 1)); if (typeof val !== 'undefined' && returnValue === null) { returnValue = val; } } else { const val = await onNext(); if (typeof val !== 'undefined') { returnValue = val; } } } logger.info(`${type} handler called`); const handlers = this.handlers[type] || []; await runHandler(0); return returnValue; } /** * Creates an InvokeResponse object. * This static method creates an invoke response with the specified body. * @param body - The body of the response * @returns An invoke response object with status and body * @protected */ static createInvokeResponse(body) { return { status: 200, body }; } /** * Dispatches the Event activity. * This method dispatches event activities to the appropriate handlers. * @param context - The turn context for the current turn of conversation * @protected */ async dispatchEventActivity(context) { if (context.activity.name === tokenResponseEventName_1.tokenResponseEventName) { await this.handle(context, 'TokenResponseEvent', this.defaultNextEvent(context)); } else { await this.defaultNextEvent(context)(); } } } exports.ActivityHandler = ActivityHandler; //# sourceMappingURL=activityHandler.js.map