@microsoft/agents-hosting
Version:
Microsoft 365 Agents SDK for JavaScript
518 lines • 26.6 kB
JavaScript
;
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CloudAdapter = void 0;
const activityHandler_1 = require("./activityHandler");
const baseAdapter_1 = require("./baseAdapter");
const turnContext_1 = require("./turnContext");
const connectorClient_1 = require("./connector-client/connectorClient");
const authConfiguration_1 = require("./auth/authConfiguration");
const authConstants_1 = require("./auth/authConstants");
const msalConnectionManager_1 = require("./auth/msalConnectionManager");
const agents_activity_1 = require("@microsoft/agents-activity");
const uuid = __importStar(require("uuid"));
const logger_1 = require("@microsoft/agents-activity/logger");
const statusCodes_1 = require("./statusCodes");
const activityWireCompat_1 = require("./activityWireCompat");
const oauth_1 = require("./oauth");
const headerPropagation_1 = require("./headerPropagation");
const customUserTokenAPI_1 = require("./oauth/customUserTokenAPI");
const logger = (0, logger_1.debug)('agents:cloud-adapter');
/**
* Adapter for handling agent interactions with various channels through cloud-based services.
*
* @remarks
* CloudAdapter processes incoming HTTP requests from Azure Bot Service channels,
* authenticates them, and generates outgoing responses. It manages the communication
* flow between agents and users across different channels, handling activities, attachments,
* and conversation continuations.
*/
class CloudAdapter extends baseAdapter_1.BaseAdapter {
/**
* Creates an instance of CloudAdapter.
* @param authConfig - The authentication configuration for securing communications
* @param authProvider - No longer used
*/
constructor(authConfig, authProvider, userTokenClient) {
super();
authConfig = (0, authConfiguration_1.getAuthConfigWithDefaults)(authConfig);
this.connectionManager = new msalConnectionManager_1.MsalConnectionManager(undefined, undefined, authConfig);
}
/**
* Determines whether a connector client is needed based on the delivery mode and service URL of the given activity.
*
* @param activity - The activity to evaluate.
* @returns true if a ConnectorClient is needed, false otherwise.
* A connector client is required if the activity's delivery mode is not "ExpectReplies"
* and the service URL is not null or empty.
* @protected
*/
resolveIfConnectorClientIsNeeded(activity) {
if (!activity) {
throw new TypeError('`activity` parameter required');
}
switch (activity.deliveryMode) {
case agents_activity_1.DeliveryModes.ExpectReplies:
if (!activity.serviceUrl) {
logger.debug('DeliveryMode = ExpectReplies, connector client is not needed');
return false;
}
break;
default:
break;
}
return true;
}
/**
* Creates a connector client for a specific service URL and scope.
*
* @param serviceUrl - The URL of the service to connect to
* @param scope - The authentication scope to use
* @param headers - Optional headers to propagate in the request
* @returns A promise that resolves to a ConnectorClient instance
* @protected
*/
async createConnectorClient(serviceUrl, scope, identity, headers) {
// get the correct token provider
const tokenProvider = this.connectionManager.getTokenProvider(identity, serviceUrl);
const token = await tokenProvider.getAccessToken(scope);
return connectorClient_1.ConnectorClient.createClientWithToken(serviceUrl, token, headers);
}
async createConnectorClientWithIdentity(identity, activity, headers) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
if (!(identity === null || identity === void 0 ? void 0 : identity.aud)) {
// anonymous
return connectorClient_1.ConnectorClient.createClientWithToken(activity.serviceUrl, null, headers);
}
let connectorClient;
const tokenProvider = this.connectionManager.getTokenProviderFromActivity(identity, activity);
if (activity.isAgenticRequest()) {
logger.debug('Activity is from an agentic source, using special scope', activity.recipient);
const agenticInstanceId = activity.getAgenticInstanceId();
const agenticUserId = activity.getAgenticUser();
if (((_b = (_a = activity.recipient) === null || _a === void 0 ? void 0 : _a.role) === null || _b === void 0 ? void 0 : _b.toLowerCase()) === agents_activity_1.RoleTypes.AgenticIdentity.toLowerCase() && agenticInstanceId) {
// get agentic instance token
const token = await tokenProvider.getAgenticInstanceToken((_c = activity.getAgenticTenantId()) !== null && _c !== void 0 ? _c : '', agenticInstanceId);
connectorClient = connectorClient_1.ConnectorClient.createClientWithToken(activity.serviceUrl, token, headers);
}
else if (((_e = (_d = activity.recipient) === null || _d === void 0 ? void 0 : _d.role) === null || _e === void 0 ? void 0 : _e.toLowerCase()) === agents_activity_1.RoleTypes.AgenticUser.toLowerCase() && agenticInstanceId && agenticUserId) {
const scope = (_g = (_f = tokenProvider.connectionSettings) === null || _f === void 0 ? void 0 : _f.scope) !== null && _g !== void 0 ? _g : authConstants_1.ApxProductionScope;
const token = await tokenProvider.getAgenticUserToken((_h = activity.getAgenticTenantId()) !== null && _h !== void 0 ? _h : '', agenticInstanceId, agenticUserId, [scope]);
connectorClient = connectorClient_1.ConnectorClient.createClientWithToken(activity.serviceUrl, token, headers);
}
else {
throw new Error('Could not create connector client for agentic user');
}
}
else {
// ABS tokens will not have an azp/appid so use the botframework scope.
// Otherwise use the appId. This will happen when communicating back to another agent.
const scope = (_k = (_j = identity.azp) !== null && _j !== void 0 ? _j : identity.appid) !== null && _k !== void 0 ? _k : 'https://api.botframework.com';
const token = await tokenProvider.getAccessToken(scope);
connectorClient = connectorClient_1.ConnectorClient.createClientWithToken(activity.serviceUrl, token, headers);
}
return connectorClient;
}
static createIdentity(appId) {
return {
aud: appId
};
}
/**
* Sets the connector client on the turn context.
*
* @param context - The current turn context
* @protected
*/
setConnectorClient(context, connectorClient) {
context.turnState.set(this.ConnectorClientKey, connectorClient);
}
/**
* Creates a user token client for a specific service URL and scope.
*
* @param serviceUrl - The URL of the service to connect to
* @param scope - The authentication scope to use
* @param headers - Optional headers to propagate in the request
* @returns A promise that resolves to a ConnectorClient instance
* @protected
*/
async createUserTokenClient(identity, tokenServiceEndpoint = (0, customUserTokenAPI_1.getTokenServiceEndpoint)(), scope = 'https://api.botframework.com', audience = 'https://api.botframework.com', headers) {
if (!(identity === null || identity === void 0 ? void 0 : identity.aud)) {
// anonymous
return oauth_1.UserTokenClient.createClientWithScope(tokenServiceEndpoint, null, scope, headers);
}
// get the correct token provider
const tokenProvider = this.connectionManager.getTokenProvider(identity, tokenServiceEndpoint);
return oauth_1.UserTokenClient.createClientWithScope(tokenServiceEndpoint, tokenProvider, scope, headers);
}
/**
* Sets the user token client on the turn context.
*
* @param context - The current turn context
* @protected
*/
setUserTokenClient(context, userTokenClient) {
context.turnState.set(this.UserTokenClientKey, userTokenClient);
}
/**
* @deprecated This function will not be supported in future versions. Create TurnContext directly.
* Creates a TurnContext for the given activity and logic.
* @param activity - The activity to process.
* @param logic - The logic to execute.
* @returns The created TurnContext.
*/
createTurnContext(activity, logic, identity) {
return new turnContext_1.TurnContext(this, activity, identity);
}
/**
* Sends multiple activities to the conversation.
* @param context - The TurnContext for the current turn.
* @param activities - The activities to send.
* @returns A promise representing the array of ResourceResponses for the sent activities.
*/
async sendActivities(context, activities) {
var _a;
if (!context) {
throw new TypeError('`context` parameter required');
}
if (!activities) {
throw new TypeError('`activities` parameter required');
}
if (activities.length === 0) {
throw new Error('Expecting one or more activities, but the array was empty.');
}
const responses = [];
for (const activity of activities) {
delete activity.id;
let response = { id: '' };
if (activity.type === agents_activity_1.ActivityTypes.InvokeResponse) {
context.turnState.set(activityHandler_1.INVOKE_RESPONSE_KEY, activity);
}
else if (activity.type === agents_activity_1.ActivityTypes.Trace && activity.channelId !== agents_activity_1.Channels.Emulator) {
// no-op
}
else {
if (!activity.serviceUrl || (activity.conversation == null) || !activity.conversation.id) {
throw new Error('Invalid activity object');
}
if (activity.replyToId) {
response = await context.turnState.get(this.ConnectorClientKey).replyToActivity(activity.conversation.id, activity.replyToId, activity);
}
else {
response = await context.turnState.get(this.ConnectorClientKey).sendToConversation(activity.conversation.id, activity);
}
}
if (!response) {
response = { id: (_a = activity.id) !== null && _a !== void 0 ? _a : '' };
}
responses.push(response);
}
return responses;
}
/**
* Processes an incoming request and sends the response.
* @param request - The incoming request.
* @param res - The response to send.
* @param logic - The logic to execute.
* @param headerPropagation - Optional function to handle header propagation.
*/
async process(request, res, logic, headerPropagation) {
var _a, _b;
const headers = new headerPropagation_1.HeaderPropagation(request.headers);
if (headerPropagation && typeof headerPropagation === 'function') {
headerPropagation(headers);
logger.debug('Headers to propagate: ', headers);
}
const end = (status, body, isInvokeResponseOrExpectReplies = false) => {
res.status(status);
if (isInvokeResponseOrExpectReplies) {
res.setHeader('content-type', 'application/json');
}
if (body) {
res.send(body);
}
res.end();
};
if (!request.body) {
throw new TypeError('`request.body` parameter required, make sure express.json() is used as middleware');
}
const incoming = (0, activityWireCompat_1.normalizeIncomingActivity)(request.body);
const activity = agents_activity_1.Activity.fromObject(incoming);
logger.info(`--> Processing incoming activity, type:${activity.type} channel:${activity.channelId}`);
if (!this.isValidChannelActivity(activity)) {
return end(statusCodes_1.StatusCodes.BAD_REQUEST);
}
logger.debug('Received activity: ', activity);
const context = new turnContext_1.TurnContext(this, activity, request.user);
// if Delivery Mode == ExpectReplies, we don't need a connector client.
if (this.resolveIfConnectorClientIsNeeded(activity)) {
const connectorClient = await this.createConnectorClientWithIdentity(request.user, activity, headers);
this.setConnectorClient(context, connectorClient);
}
if (!activity.isAgenticRequest()) {
const userTokenClient = await this.createUserTokenClient(request.user);
this.setUserTokenClient(context, userTokenClient);
}
if ((activity === null || activity === void 0 ? void 0 : activity.type) === agents_activity_1.ActivityTypes.InvokeResponse ||
(activity === null || activity === void 0 ? void 0 : activity.type) === agents_activity_1.ActivityTypes.Invoke ||
(activity === null || activity === void 0 ? void 0 : activity.deliveryMode) === agents_activity_1.DeliveryModes.ExpectReplies) {
await this.runMiddleware(context, logic);
const invokeResponse = this.processTurnResults(context);
logger.debug('Activity Response (invoke/expect replies): ', invokeResponse);
return end((_a = invokeResponse === null || invokeResponse === void 0 ? void 0 : invokeResponse.status) !== null && _a !== void 0 ? _a : statusCodes_1.StatusCodes.OK, invokeResponse === null || invokeResponse === void 0 ? void 0 : invokeResponse.body, true);
}
await this.runMiddleware(context, logic);
const invokeResponse = this.processTurnResults(context);
return end((_b = invokeResponse === null || invokeResponse === void 0 ? void 0 : invokeResponse.status) !== null && _b !== void 0 ? _b : statusCodes_1.StatusCodes.OK, invokeResponse === null || invokeResponse === void 0 ? void 0 : invokeResponse.body);
}
isValidChannelActivity(activity) {
var _a, _b;
if (activity == null) {
logger.warn('BadRequest: Missing activity');
return false;
}
if (activity.type == null || activity.type === '') {
logger.warn('BadRequest: Missing activity type');
return false;
}
if (((_a = activity.conversation) === null || _a === void 0 ? void 0 : _a.id) == null || ((_b = activity.conversation) === null || _b === void 0 ? void 0 : _b.id) === '') {
logger.warn('BadRequest: Missing conversation.Id');
return false;
}
return true;
}
/**
* Updates an activity.
* @param context - The TurnContext for the current turn.
* @param activity - The activity to update.
* @returns A promise representing the ResourceResponse for the updated activity.
*/
async updateActivity(context, activity) {
if (!context) {
throw new TypeError('`context` parameter required');
}
if (!activity) {
throw new TypeError('`activity` parameter required');
}
if (!activity.serviceUrl || (activity.conversation == null) || !activity.conversation.id || !activity.id) {
throw new Error('Invalid activity object');
}
const response = await context.turnState.get(this.ConnectorClientKey).updateActivity(activity.conversation.id, activity.id, activity);
return response.id ? { id: response.id } : undefined;
}
/**
* Deletes an activity.
* @param context - The TurnContext for the current turn.
* @param reference - The conversation reference of the activity to delete.
* @returns A promise representing the completion of the delete operation.
*/
async deleteActivity(context, reference) {
if (!context) {
throw new TypeError('`context` parameter required');
}
if (!reference || !reference.serviceUrl || (reference.conversation == null) || !reference.conversation.id || !reference.activityId) {
throw new Error('Invalid conversation reference object');
}
await context.turnState.get(this.ConnectorClientKey).deleteActivity(reference.conversation.id, reference.activityId);
}
/**
* Continues a conversation.
* @param reference - The conversation reference to continue.
* @param logic - The logic to execute.
* @returns A promise representing the completion of the continue operation.
*/
async continueConversation(botAppIdOrIdentity, reference, logic, isResponse = false) {
if (!reference || !reference.serviceUrl || (reference.conversation == null) || !reference.conversation.id) {
throw new Error('continueConversation: Invalid conversation reference object');
}
if (!botAppIdOrIdentity) {
throw new TypeError('continueConversation: botAppIdOrIdentity is required');
}
const botAppId = typeof botAppIdOrIdentity === 'string' ? botAppIdOrIdentity : botAppIdOrIdentity.aud;
// Only having the botId will only work against ABS or Agentic. Proactive to other agents will
// not work with just botId. Use a JwtPayload with property aud (which is botId) and appid populated.
const identity = typeof botAppIdOrIdentity !== 'string'
? botAppIdOrIdentity
: CloudAdapter.createIdentity(botAppId);
const context = new turnContext_1.TurnContext(this, agents_activity_1.Activity.getContinuationActivity(reference), identity);
const connectorClient = await this.createConnectorClientWithIdentity(identity, context.activity);
this.setConnectorClient(context, connectorClient);
if (!context.activity.isAgenticRequest()) {
const userTokenClient = await this.createUserTokenClient(identity);
this.setUserTokenClient(context, userTokenClient);
}
await this.runMiddleware(context, logic);
}
/**
* Processes the turn results and returns an InvokeResponse if applicable.
* @param context - The TurnContext for the current turn.
* @returns The InvokeResponse if applicable, otherwise undefined.
*/
processTurnResults(context) {
logger.info('<--Sending back turn results');
// Handle ExpectedReplies scenarios where all activities have been buffered and sent back at once in an invoke response.
if (context.activity.deliveryMode === agents_activity_1.DeliveryModes.ExpectReplies) {
return {
status: statusCodes_1.StatusCodes.OK,
body: {
activities: context.bufferedReplyActivities
}
};
}
// Handle Invoke scenarios where the agent will return a specific body and return code.
if (context.activity.type === agents_activity_1.ActivityTypes.Invoke) {
const activityInvokeResponse = context.turnState.get(activityHandler_1.INVOKE_RESPONSE_KEY);
if (!activityInvokeResponse) {
return { status: statusCodes_1.StatusCodes.NOT_IMPLEMENTED };
}
return activityInvokeResponse.value;
}
// No body to return.
return undefined;
}
/**
* Creates an activity to represent the result of creating a conversation.
* @param createdConversationId - The ID of the created conversation.
* @param channelId - The channel ID.
* @param serviceUrl - The service URL.
* @param conversationParameters - The conversation parameters.
* @returns The created activity.
*/
createCreateActivity(createdConversationId, channelId, serviceUrl, conversationParameters) {
// Create a conversation update activity to represent the result.
const activity = new agents_activity_1.Activity(agents_activity_1.ActivityTypes.Event);
activity.name = agents_activity_1.ActivityEventNames.CreateConversation;
activity.channelId = channelId;
activity.serviceUrl = serviceUrl;
activity.id = createdConversationId !== null && createdConversationId !== void 0 ? createdConversationId : uuid.v4();
activity.conversation = {
conversationType: undefined,
id: createdConversationId,
isGroup: conversationParameters.isGroup,
name: undefined,
tenantId: conversationParameters.tenantId,
};
activity.channelData = conversationParameters.channelData;
activity.recipient = conversationParameters.agent;
return activity;
}
/**
* Creates a conversation.
* @param agentAppId - The agent application ID.
* @param channelId - The channel ID.
* @param serviceUrl - The service URL.
* @param audience - The audience.
* @param conversationParameters - The conversation parameters.
* @param logic - The logic to execute.
* @returns A promise representing the completion of the create operation.
*/
async createConversationAsync(agentAppId, channelId, serviceUrl, audience, conversationParameters, logic) {
if (typeof serviceUrl !== 'string' || !serviceUrl) {
throw new TypeError('`serviceUrl` must be a non-empty string');
}
if (!conversationParameters)
throw new TypeError('`conversationParameters` must be defined');
if (!logic)
throw new TypeError('`logic` must be defined');
const identity = CloudAdapter.createIdentity(audience);
const restClient = await this.createConnectorClient(serviceUrl, audience, identity);
const userTokenClient = await this.createUserTokenClient(identity);
const createConversationResult = await restClient.createConversation(conversationParameters);
const createActivity = this.createCreateActivity(createConversationResult.id, channelId, serviceUrl, conversationParameters);
const context = new turnContext_1.TurnContext(this, createActivity, CloudAdapter.createIdentity(agentAppId));
this.setConnectorClient(context, restClient);
this.setUserTokenClient(context, userTokenClient);
await this.runMiddleware(context, logic);
}
/**
* @deprecated This function will not be supported in future versions. Use TurnContext.turnState.get<ConnectorClient>(CloudAdapter.ConnectorClientKey).
* Uploads an attachment.
* @param conversationId - The conversation ID.
* @param attachmentData - The attachment data.
* @returns A promise representing the ResourceResponse for the uploaded attachment.
*/
async uploadAttachment(context, conversationId, attachmentData) {
if (context === undefined) {
throw new Error('context is required');
}
if (conversationId === undefined) {
throw new Error('conversationId is required');
}
if (attachmentData === undefined) {
throw new Error('attachmentData is required');
}
return await context.turnState.get(this.ConnectorClientKey).uploadAttachment(conversationId, attachmentData);
}
/**
* @deprecated This function will not be supported in future versions. Use TurnContext.turnState.get<ConnectorClient>(CloudAdapter.ConnectorClientKey).
* Gets attachment information.
* @param attachmentId - The attachment ID.
* @returns A promise representing the AttachmentInfo for the requested attachment.
*/
async getAttachmentInfo(context, attachmentId) {
if (context === undefined) {
throw new Error('context is required');
}
if (attachmentId === undefined) {
throw new Error('attachmentId is required');
}
return await context.turnState.get(this.ConnectorClientKey).getAttachmentInfo(attachmentId);
}
/**
* @deprecated This function will not be supported in future versions. Use TurnContext.turnState.get<ConnectorClient>(CloudAdapter.ConnectorClientKey).
* Gets an attachment.
* @param attachmentId - The attachment ID.
* @param viewId - The view ID.
* @returns A promise representing the NodeJS.ReadableStream for the requested attachment.
*/
async getAttachment(context, attachmentId, viewId) {
if (context === undefined) {
throw new Error('context is required');
}
if (attachmentId === undefined) {
throw new Error('attachmentId is required');
}
if (viewId === undefined) {
throw new Error('viewId is required');
}
return await context.turnState.get(this.ConnectorClientKey).getAttachment(attachmentId, viewId);
}
}
exports.CloudAdapter = CloudAdapter;
//# sourceMappingURL=cloudAdapter.js.map