@microsoft/agents-copilotstudio-client
Version:
Microsoft Copilot Studio Client for JavaScript. Copilot Studio Client.
399 lines (398 loc) • 17.5 kB
JavaScript
;
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CopilotStudioWebChat = void 0;
const uuid_1 = require("uuid");
const agents_activity_1 = require("@microsoft/agents-activity");
const rxjs_1 = require("rxjs");
const agents_telemetry_1 = require("@microsoft/agents-telemetry");
const observability_1 = require("./observability");
const logger = (0, agents_telemetry_1.debug)('copilot-studio:webchat');
/**
* Creates a wrapper that invokes `fn` at most once.
* On the first call the wrapper invokes `fn(value)` and returns whatever `fn` returns.
* Subsequent calls do nothing and return `undefined`.
*
* @template T - Type of the single argument passed to the wrapped function.
* @param fn Function to be invoked once.
* @returns A wrapper function that calls `fn` at most once.
*/
function once(fn) {
let called = false;
return value => {
if (!called) {
called = true;
return fn(value);
}
};
}
/**
* A utility class that provides WebChat integration capabilities for Copilot Studio services.
*
* @remarks
* This class acts as a bridge between Microsoft Bot Framework WebChat and Copilot Studio,
* enabling seamless communication through a DirectLine-compatible interface.
*
* ## Key Features:
* - DirectLine protocol compatibility for easy WebChat integration
* - Real-time bidirectional messaging with Copilot Studio agents
* - Automatic conversation management and message sequencing
* - Optional typing indicators for enhanced user experience
* - Observable-based architecture for reactive programming patterns
*
* ## Usage Scenarios:
* - Embedding Copilot Studio agents in web applications
* - Creating custom chat interfaces with WebChat components
* - Building conversational AI experiences with Microsoft's bot ecosystem
*
* @example Basic WebChat Integration
* ```typescript
* import { CopilotStudioClient } from '@microsoft/agents-copilotstudio-client';
* import { CopilotStudioWebChat } from '@microsoft/agents-copilotstudio-client';
*
* // Initialize the Copilot Studio client
* const client = new CopilotStudioClient({
* botId: 'your-bot-id',
* tenantId: 'your-tenant-id'
* });
*
* // Create a WebChat-compatible connection
* const directLine = CopilotStudioWebChat.createConnection(client, {
* showTyping: true
* });
*
* // Integrate with WebChat
* window.WebChat.renderWebChat({
* directLine: directLine,
* // ... other WebChat options
* }, document.getElementById('webchat'));
* ```
*
* @example Advanced Usage with Connection Monitoring
* ```typescript
* const connection = CopilotStudioWebChat.createConnection(client);
*
* // Monitor connection status
* connection.connectionStatus$.subscribe(status => {
* switch (status) {
* case 0: console.log('Disconnected'); break;
* case 1: console.log('Connecting...'); break;
* case 2: console.log('Connected and ready'); break;
* }
* });
*
* // Listen for incoming activities
* connection.activity$.subscribe(activity => {
* console.log('Received activity:', activity);
* });
* ```
*/
class CopilotStudioWebChat {
/**
* Creates a DirectLine-compatible connection for integrating Copilot Studio with WebChat.
*
* @param client - A configured CopilotStudioClient instance that handles the underlying
* communication with the Copilot Studio service. This client should be
* properly authenticated and configured with the target bot details.
*
* @param settings - Optional configuration settings that control the behavior of the
* WebChat connection. These settings allow customization of features
* like typing indicators and other user experience enhancements.
*
* @returns A new CopilotStudioWebChatConnection instance that can be passed directly
* to WebChat's renderWebChat function as the directLine parameter. The
* connection is immediately ready for use and will automatically manage
* the conversation lifecycle.
*
* @throws Error if the provided client is not properly configured or if there are
* issues establishing the initial connection to the Copilot Studio service.
*
* @remarks
* This method establishes a real-time communication channel between WebChat and the
* Copilot Studio service. The returned connection object implements the DirectLine
* protocol, making it fully compatible with Microsoft Bot Framework WebChat components.
*
* ## Connection Lifecycle:
* 1. **Initialization**: Creates observables for connection status and activity streaming
* 2. **Conversation Start**: Automatically initiates conversation when first activity is posted
* 3. **Message Flow**: Handles bidirectional message exchange with proper sequencing
* 4. **Cleanup**: Provides graceful connection termination
*
* ## Message Processing:
* - User messages are validated and sent to Copilot Studio
* - Agent responses are received and formatted for WebChat
* - All activities include timestamps and sequence IDs for proper ordering
* - Optional typing indicators provide visual feedback during processing
*
* @example
* ```typescript
* const connection = CopilotStudioWebChat.createConnection(client, {
* showTyping: true
* });
*
* // Use with WebChat
* window.WebChat.renderWebChat({
* directLine: connection
* }, document.getElementById('webchat'));
* ```
*/
static createConnection(client, settings) {
var _a;
const managed = (0, agents_telemetry_1.trace)(observability_1.CopilotStudioClientTraceDefinitions.createConnection);
managed.record({ showTyping: settings === null || settings === void 0 ? void 0 : settings.showTyping });
try {
logger.info('--> Creating connection between Copilot Studio and WebChat ...');
const normalizedConversationId = (settings === null || settings === void 0 ? void 0 : settings.conversationId) && settings.conversationId.trim() !== ''
? settings.conversationId.trim()
: undefined;
const shouldStart = (_a = settings === null || settings === void 0 ? void 0 : settings.startConversation) !== null && _a !== void 0 ? _a : !normalizedConversationId;
let sequence = 0;
let activitySubscriber;
let conversation;
let activeConversationId = normalizedConversationId;
let ended = false;
let started = false;
const connectionStatus$ = new rxjs_1.BehaviorSubject(0);
const activity$ = createObservable(async (subscriber) => {
var _a;
try {
activitySubscriber = subscriber;
const handleAcknowledgementOnce = once(async () => {
connectionStatus$.next(2);
await Promise.resolve(); // Webchat requires an extra tick to process the connection status change
});
// When resuming (shouldStart === false), transition straight to connected
if (!shouldStart || started) {
await handleAcknowledgementOnce();
return;
}
started = true;
logger.debug('--> Connection established.');
notifyTyping();
for await (const activity of client.startConversationStreaming()) {
delete activity.replyToId;
if (!conversation && activity.conversation) {
conversation = activity.conversation;
}
if ((_a = activity.conversation) === null || _a === void 0 ? void 0 : _a.id) {
activeConversationId = activity.conversation.id;
}
await handleAcknowledgementOnce();
notifyActivity(activity);
managed.actions.receivedFromCopilot(activity);
}
// If no activities received from bot, we should still acknowledge.
await handleAcknowledgementOnce();
}
catch (error) {
throw managed.fail(error);
}
finally {
managed.end();
}
});
const notifyActivity = (activity) => {
const newActivity = {
...activity,
timestamp: new Date().toISOString(),
channelData: {
...activity.channelData,
'webchat:sequence-id': sequence,
},
};
sequence++;
logger.debug(`Notify '${newActivity.type}' activity to WebChat:`, newActivity);
activitySubscriber === null || activitySubscriber === void 0 ? void 0 : activitySubscriber.next(newActivity);
};
const notifyTyping = () => {
if (!(settings === null || settings === void 0 ? void 0 : settings.showTyping)) {
return;
}
const from = conversation
? { id: conversation.id, name: conversation.name }
: { id: 'agent', name: 'Agent' };
notifyActivity({ type: 'typing', from });
};
return {
connectionStatus$,
activity$,
get conversationId() {
return activeConversationId;
},
postActivity(activity) {
try {
logger.info('--> Preparing to send activity to Copilot Studio ...');
if (!activity) {
throw new Error('Activity cannot be null.');
}
if (ended) {
throw new Error('Connection has been ended.');
}
if (!activitySubscriber) {
throw new Error('Activity subscriber is not initialized.');
}
const result = createObservable(async (subscriber) => {
var _a;
try {
logger.info('--> Sending activity to Copilot Studio ...');
const newActivity = agents_activity_1.Activity.fromObject({
...activity,
id: (0, uuid_1.v4)(),
attachments: await processAttachments(activity)
});
notifyActivity(newActivity);
managed.actions.sentToWebChat(newActivity);
notifyTyping();
// Notify WebChat immediately that the message was sent
subscriber.next(newActivity.id);
// Stream the agent's response, passing activeConversationId for URL routing
for await (const responseActivity of client.sendActivityStreaming(newActivity, activeConversationId)) {
if (!activeConversationId && ((_a = responseActivity.conversation) === null || _a === void 0 ? void 0 : _a.id)) {
activeConversationId = responseActivity.conversation.id;
}
notifyActivity(responseActivity);
managed.actions.receivedFromCopilot(responseActivity);
logger.info('<-- Activity received correctly from Copilot Studio.');
}
subscriber.complete();
}
catch (error) {
logger.error('Error sending Activity to Copilot Studio:', error);
subscriber.error(error);
managed.fail(error);
}
finally {
managed.end();
}
});
return result;
}
catch (error) {
throw managed.fail(error);
}
finally {
managed.end();
}
},
end() {
logger.info('--> Ending connection between Copilot Studio and WebChat ...');
ended = true;
connectionStatus$.complete();
if (activitySubscriber) {
activitySubscriber.complete();
activitySubscriber = undefined;
}
// End the connection span
managed.end();
},
};
}
catch (error) {
throw managed.fail(error);
}
finally {
managed.end();
}
}
}
exports.CopilotStudioWebChat = CopilotStudioWebChat;
/**
* Processes activity attachments.
* @param activity The activity to process for attachments.
* @returns A promise that resolves to the activity with all attachments converted.
*/
async function processAttachments(activity) {
var _a;
if (activity.type !== 'message' || !((_a = activity.attachments) === null || _a === void 0 ? void 0 : _a.length)) {
return activity.attachments || [];
}
const attachments = [];
for (const attachment of activity.attachments) {
const processed = await processBlobAttachment(attachment);
attachments.push(processed);
}
return attachments;
}
/**
* Processes a blob attachment to convert its content URL to a data URL.
* @param attachment The attachment to process.
* @returns A promise that resolves to the processed attachment.
*/
async function processBlobAttachment(attachment) {
let newContentUrl = attachment.contentUrl;
if (!(newContentUrl === null || newContentUrl === void 0 ? void 0 : newContentUrl.startsWith('blob:'))) {
return attachment;
}
try {
const response = await fetch(newContentUrl);
if (!response.ok) {
throw new Error(`Failed to fetch blob URL: ${response.status} ${response.statusText}`);
}
const blob = await response.blob();
const arrayBuffer = await blob.arrayBuffer();
const base64 = arrayBufferToBase64(arrayBuffer);
newContentUrl = `data:${blob.type};base64,${base64}`;
}
catch (error) {
newContentUrl = attachment.contentUrl;
logger.error('Error processing blob attachment:', newContentUrl, error);
}
return { ...attachment, contentUrl: newContentUrl };
}
/**
* Converts an ArrayBuffer to a base64 string.
* @param buffer The ArrayBuffer to convert.
* @returns The base64 encoded string.
*/
function arrayBufferToBase64(buffer) {
// Node.js environment
const BufferClass = typeof globalThis.Buffer === 'function' ? globalThis.Buffer : undefined;
if (BufferClass && typeof BufferClass.from === 'function') {
return BufferClass.from(buffer).toString('base64');
}
// Browser environment
let binary = '';
for (const byte of new Uint8Array(buffer)) {
binary += String.fromCharCode(byte);
}
return btoa(binary);
}
/**
* Creates an RxJS Observable that wraps an asynchronous function execution.
*
* @typeParam T - The type of value that the observable will emit
* @param fn - An asynchronous function that receives a Subscriber and performs
* the desired async operation. The function should call subscriber.next()
* with results and subscriber.complete() when finished.
* @returns A new Observable that executes the provided function and emits its results
*
* @remarks
* This utility function provides a clean way to convert async/await patterns
* into Observable streams, enabling integration with reactive programming patterns
* used throughout the WebChat connection implementation.
*
* The created Observable handles promise resolution and rejection automatically,
* converting them to appropriate next/error signals for subscribers.
*
* @example
* ```typescript
* const dataObservable = createObservable<string>(async (subscriber) => {
* try {
* const result = await fetchData();
* subscriber.next(result);
* subscriber.complete();
* } catch (error) {
* subscriber.error(error);
* }
* });
* ```
*/
function createObservable(fn) {
return new rxjs_1.Observable((subscriber) => {
Promise.resolve(fn(subscriber)).catch((error) => subscriber.error(error));
});
}
//# sourceMappingURL=copilotStudioWebChat.js.map