UNPKG

@microsoft/agents-copilotstudio-client

Version:

Microsoft Copilot Studio Client for JavaScript. Copilot Studio Client.

444 lines 22.3 kB
"use strict"; /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CopilotStudioClient = void 0; const eventsource_client_1 = require("eventsource-client"); const powerPlatformEnvironment_1 = require("./powerPlatformEnvironment"); const agents_activity_1 = require("@microsoft/agents-activity"); const executeTurnRequest_1 = require("./executeTurnRequest"); const agents_telemetry_1 = require("@microsoft/agents-telemetry"); const userAgentHelper_1 = require("./userAgentHelper"); const scopeHelper_1 = require("./scopeHelper"); const responses_1 = require("./responses"); const observability_1 = require("./observability"); const logger = (0, agents_telemetry_1.debug)('copilot-studio:client'); /** * Client for interacting with Microsoft Copilot Studio services. * Provides functionality to start conversations and send messages to Copilot Studio bots. */ class CopilotStudioClient { /** * Creates an instance of CopilotStudioClient. * @param settings The connection settings. * @param token The authentication token. */ constructor(settings, token) { /** The ID of the current conversation. */ this.conversationId = ''; this.settings = settings; this.token = token; } /** * Logs a diagnostic message if diagnostics are enabled. * @param message The message to log. * @param args Additional arguments to log. */ logDiagnostic(message, ...args) { if (this.settings.enableDiagnostics) { logger.info(`[DIAGNOSTICS] ${message}`, ...args); } } /** * Streams activities from the Copilot Studio service using eventsource-client. * @param url The connection URL for Copilot Studio. * @param body Optional. The request body (for POST). * @param method Optional. The HTTP method (default: POST). * @returns An async generator yielding the Agent's Activities. */ async *postRequestAsync(url, body, method = 'POST') { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; const managed = (0, agents_telemetry_1.trace)(observability_1.CopilotStudioClientTraceDefinitions.postRequest); managed.record({ url, method }); try { this.logDiagnostic(`Request URL: ${url}`); this.logDiagnostic(`Request Method: ${method}`); this.logDiagnostic('Request Body:', body ? JSON.stringify(body, null, 2) : 'none'); logger.debug(`>>> SEND TO ${url}`); const streamMap = new Map(); const eventSource = (0, eventsource_client_1.createEventSource)({ url, headers: { Authorization: `Bearer ${this.token}`, 'User-Agent': userAgentHelper_1.UserAgentHelper.getProductInfo(), 'Content-Type': 'application/json', Accept: 'text/event-stream' }, body: body ? JSON.stringify(body) : undefined, method, fetch: async (url, init) => { const response = await fetch(url, init); this.processResponseHeaders(response.headers); return response; } }); try { for await (const { data, event } of eventSource) { if (data && event === 'activity') { try { const activity = agents_activity_1.Activity.fromJson(data); managed.actions.receivedFromCopilot(activity); // check to see if this activity is part of the streamed response, in which case we need to accumulate the text const streamingEntity = (_a = activity.entities) === null || _a === void 0 ? void 0 : _a.find(e => e.type === 'streaminfo' && e.streamType === 'streaming'); switch (activity.type) { case agents_activity_1.ActivityTypes.Message: if (!this.conversationId.trim()) { // Did not get it from the header. this.conversationId = (_c = (_b = activity.conversation) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : ''; logger.debug(`Conversation ID: ${this.conversationId}`); } yield activity; break; case agents_activity_1.ActivityTypes.Typing: logger.debug(`Activity type: ${activity.type}`); // Accumulate the text as it comes in from the stream. // This also accounts for the "old style" of streaming where the stream info is in channelData. if (streamingEntity || ((_d = activity.channelData) === null || _d === void 0 ? void 0 : _d.streamType) === 'streaming') { const text = (_e = activity.text) !== null && _e !== void 0 ? _e : ''; const id = ((_f = streamingEntity === null || streamingEntity === void 0 ? void 0 : streamingEntity.streamId) !== null && _f !== void 0 ? _f : (_g = activity.channelData) === null || _g === void 0 ? void 0 : _g.streamId); const sequence = ((_h = streamingEntity === null || streamingEntity === void 0 ? void 0 : streamingEntity.streamSequence) !== null && _h !== void 0 ? _h : (_j = activity.channelData) === null || _j === void 0 ? void 0 : _j.streamSequence); // Accumulate the text chunks based on stream ID and sequence number. if (id && sequence) { if (streamMap.has(id)) { const existing = streamMap.get(id); existing.push({ text, sequence }); streamMap.set(id, existing); } else { streamMap.set(id, [{ text, sequence }]); } activity.text = ((_k = streamMap.get(id)) === null || _k === void 0 ? void 0 : _k.sort((a, b) => a.sequence - b.sequence).map(item => item.text).join('')) || ''; } } yield activity; break; default: logger.debug(`Activity type: ${activity.type}`); yield activity; break; } } catch (error) { logger.error('Failed to parse activity:', error); } } else if (event === 'end') { logger.debug('Stream complete'); break; } if (eventSource.readyState === 'closed') { logger.debug('Connection closed'); break; } } } finally { eventSource.close(); } } catch (error) { throw managed.fail(error); } finally { managed.end(); } } processResponseHeaders(responseHeaders) { var _a, _b; if (this.settings.useExperimentalEndpoint && !((_a = this.settings.directConnectUrl) === null || _a === void 0 ? void 0 : _a.trim())) { const islandExperimentalUrl = responseHeaders === null || responseHeaders === void 0 ? void 0 : responseHeaders.get(CopilotStudioClient.islandExperimentalUrlHeaderKey); if (islandExperimentalUrl) { this.settings.directConnectUrl = islandExperimentalUrl; logger.debug(`Island Experimental URL: ${islandExperimentalUrl}`); } } this.conversationId = (_b = responseHeaders === null || responseHeaders === void 0 ? void 0 : responseHeaders.get(CopilotStudioClient.conversationIdHeaderKey)) !== null && _b !== void 0 ? _b : ''; if (this.conversationId) { logger.debug(`Conversation ID: ${this.conversationId}`); } const sanitizedHeaders = new Headers(); responseHeaders.forEach((value, key) => { if (key.toLowerCase() !== 'authorization' && key.toLowerCase() !== CopilotStudioClient.conversationIdHeaderKey.toLowerCase()) { sanitizedHeaders.set(key, value); } }); this.logDiagnostic('Response Headers:', sanitizedHeaders); } /** * Implementation of startConversationStreaming with overloads. */ async *startConversationStreaming(requestOrFlag) { var _a, _b; const managed = (0, agents_telemetry_1.trace)(observability_1.CopilotStudioClientTraceDefinitions.startConversation); try { // Normalize input to StartRequest let request; if (typeof requestOrFlag === 'boolean' || requestOrFlag === undefined) { // Legacy call: startConversationStreaming(true/false) managed.record({ shouldEmitStartEvent: requestOrFlag !== null && requestOrFlag !== void 0 ? requestOrFlag : true }); request = { emitStartConversationEvent: requestOrFlag !== null && requestOrFlag !== void 0 ? requestOrFlag : true }; } else { // New call: startConversationStreaming({ locale: 'en-US', ... }) request = requestOrFlag; managed.record({ shouldEmitStartEvent: (_a = request.emitStartConversationEvent) !== null && _a !== void 0 ? _a : true }); } const uriStart = (0, powerPlatformEnvironment_1.getCopilotStudioConnectionUrl)(this.settings, request.conversationId); const body = { emitStartConversationEvent: (_b = request.emitStartConversationEvent) !== null && _b !== void 0 ? _b : true }; // Add locale to body if provided if (request.locale) { body.locale = request.locale; } logger.info('Starting conversation ...', request); this.logDiagnostic('Start conversation request:', body); yield* this.postRequestAsync(uriStart, body, 'POST'); } catch (error) { throw managed.fail(error); } finally { managed.end(); } } /** * Sends an activity to the Copilot Studio service and retrieves the response activities. * @param activity The activity to send. * @param conversationId The ID of the conversation. Defaults to the current conversation ID. * @returns An async generator yielding the Agent's Activities. */ async *sendActivityStreaming(activity, conversationId = this.conversationId) { var _a, _b; const managed = (0, agents_telemetry_1.trace)(observability_1.CopilotStudioClientTraceDefinitions.sendActivity); managed.record({ activity }); try { const localConversationId = (_b = (_a = activity.conversation) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : conversationId; const uriExecute = (0, powerPlatformEnvironment_1.getCopilotStudioConnectionUrl)(this.settings, localConversationId); const qbody = new executeTurnRequest_1.ExecuteTurnRequest(activity); logger.info('Sending activity...', activity); yield* this.postRequestAsync(uriExecute, qbody, 'POST'); } catch (error) { throw managed.fail(error); } finally { managed.end(); } } /** * Executes a turn in an existing conversation by sending an activity. * This method provides explicit control over the conversation ID. * @param activity The activity to send. * @param conversationId The ID of the conversation. Required. * @returns An async generator yielding the Agent's Activities. * @throws Error if conversationId is not provided. */ async *executeStreaming(activity, conversationId) { const managed = (0, agents_telemetry_1.trace)(observability_1.CopilotStudioClientTraceDefinitions.executeStreaming); managed.record({ activity, conversationId }); try { if (!conversationId || !conversationId.trim()) { throw new Error('conversationId is required for executeStreaming'); } const uriExecute = (0, powerPlatformEnvironment_1.getCopilotStudioConnectionUrl)(this.settings, conversationId); const request = new executeTurnRequest_1.ExecuteTurnRequest(activity, conversationId); logger.info('Executing turn with conversation ID:', conversationId); this.logDiagnostic('Execute turn request:', { conversationId, activityType: activity.type, activityText: activity.text }); yield* this.postRequestAsync(uriExecute, request, 'POST'); } catch (error) { throw managed.fail(error); } finally { managed.end(); } } /** * Executes a turn in an existing conversation by sending an activity. * @param activity The activity to send. * @param conversationId The ID of the conversation. Required. * @returns A promise yielding an array of activities. * @throws Error if conversationId is not provided. * @deprecated Use executeStreaming instead. */ async execute(activity, conversationId) { const result = []; for await (const value of this.executeStreaming(activity, conversationId)) { result.push(value); } return result; } /** * Implementation of startConversationAsync with overloads. */ async startConversationAsync(requestOrFlag) { const result = []; for await (const value of this.startConversationStreaming(requestOrFlag)) { result.push(value); } return result; } /** * Sends a question to the Copilot Studio service and retrieves the response activities. * @param question The question to ask. * @param conversationId The ID of the conversation. Defaults to the current conversation ID. * @returns A promise yielding an array of activities. * @deprecated Use sendActivityStreaming instead. */ async askQuestionAsync(question, conversationId) { const localConversationId = (conversationId === null || conversationId === void 0 ? void 0 : conversationId.trim()) ? conversationId : this.conversationId; const conversationAccount = { id: localConversationId }; const activityObj = { type: 'message', text: question, conversation: conversationAccount }; const activity = agents_activity_1.Activity.fromObject(activityObj); const result = []; for await (const value of this.sendActivityStreaming(activity, conversationId)) { result.push(value); } return result; } /** * Sends an activity to the Copilot Studio service and retrieves the response activities. * @param activity The activity to send. * @param conversationId The ID of the conversation. Defaults to the current conversation ID. * @returns A promise yielding an array of activities. * @deprecated Use sendActivityStreaming instead. */ async sendActivity(activity, conversationId = this.conversationId) { const result = []; for await (const value of this.sendActivityStreaming(activity, conversationId)) { result.push(value); } return result; } /** * Starts a new conversation and returns a typed response. * @param request The request parameters for starting the conversation. * @returns A promise yielding a StartResponse with activities and conversation metadata. */ async startConversationWithResponse(request) { var _a; const activities = []; let finalConversationId = ''; for await (const activity of this.startConversationStreaming(request)) { activities.push(activity); if ((_a = activity.conversation) === null || _a === void 0 ? void 0 : _a.id) { finalConversationId = activity.conversation.id; } } // Fall back to instance conversationId if not found in activities finalConversationId = finalConversationId || this.conversationId; return (0, responses_1.createStartResponse)(activities, finalConversationId); } /** * Executes a turn and returns a typed response. * @param activity The activity to send. * @param conversationId The conversation ID. * @returns A promise yielding an ExecuteTurnResponse with activities and metadata. */ async executeWithResponse(activity, conversationId) { const activities = []; for await (const value of this.executeStreaming(activity, conversationId)) { activities.push(value); } return (0, responses_1.createExecuteTurnResponse)(activities, conversationId); } /** * Subscribes to a conversation to receive events via Server-Sent Events (SSE). * This method allows resumption from a specific event ID. * @param conversationId The ID of the conversation to subscribe to. * @param lastReceivedEventId Optional. The last received event ID for resumption. * @returns An async generator yielding SubscribeEvent objects containing activities and event IDs. */ async *subscribeAsync(conversationId, lastReceivedEventId) { const managed = (0, agents_telemetry_1.trace)(observability_1.CopilotStudioClientTraceDefinitions.subscribeAsync); managed.record({ conversationId, lastReceivedEventId }); try { if (!conversationId || !conversationId.trim()) { throw new Error('conversationId is required for subscribeAsync'); } const url = (0, powerPlatformEnvironment_1.getCopilotStudioSubscribeUrl)(this.settings, conversationId); logger.info('Subscribing to conversation:', conversationId); this.logDiagnostic('Subscribe request:', { conversationId, lastReceivedEventId, url }); const eventSource = (0, eventsource_client_1.createEventSource)({ url, headers: { Authorization: `Bearer ${this.token}`, 'User-Agent': userAgentHelper_1.UserAgentHelper.getProductInfo(), Accept: 'text/event-stream', ...(lastReceivedEventId && { 'Last-Event-ID': lastReceivedEventId }) }, method: 'GET', fetch: async (url, init) => { const response = await fetch(url, init); this.processResponseHeaders(response.headers); return response; } }); try { for await (const { data, event, id } of eventSource) { if (data && event === 'activity') { try { const activity = agents_activity_1.Activity.fromJson(data); const subscribeEvent = { activity, eventId: id }; managed.actions.eventReceivedFromCopilot(subscribeEvent); logger.debug(`Received activity via subscription, event ID: ${id}`); this.logDiagnostic('Subscribe event received:', { eventId: id, activityType: activity.type }); yield subscribeEvent; } catch (error) { logger.error('Failed to parse activity in subscription:', error); } } else if (event === 'end') { logger.debug('Subscription stream complete'); break; } if (eventSource.readyState === 'closed') { logger.debug('Subscription connection closed'); break; } } } finally { eventSource.close(); } } catch (error) { throw managed.fail(error); } finally { managed.end(); } } } exports.CopilotStudioClient = CopilotStudioClient; /** Header key for conversation ID. */ CopilotStudioClient.conversationIdHeaderKey = 'x-ms-conversationid'; /** Island Header key */ CopilotStudioClient.islandExperimentalUrlHeaderKey = 'x-ms-d2e-experimental'; /** * Returns the scope URL needed to connect to Copilot Studio from the connection settings. * This is used for authentication token audience configuration. * @param settings Copilot Studio connection settings. * @returns The scope URL for token audience. * @deprecated Use ScopeHelper.getScopeFromSettings instead. */ CopilotStudioClient.scopeFromSettings = scopeHelper_1.ScopeHelper.getScopeFromSettings; //# sourceMappingURL=copilotStudioClient.js.map