@microsoft/agents-copilotstudio-client
Version:
Microsoft Copilot Studio Client for JavaScript. Copilot Studio Client.
444 lines • 22.3 kB
JavaScript
;
/**
* 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