@microsoft/agents-hosting
Version:
Microsoft 365 Agents SDK for JavaScript
671 lines • 27.9 kB
JavaScript
"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