UNPKG

@microsoft/teams-ai

Version:

SDK focused on building AI based applications for Microsoft Teams.

338 lines 11.7 kB
"use strict"; /** * @module teams-ai */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.StreamingResponse = void 0; const Utilities_1 = require("./Utilities"); /** * A helper class for streaming responses to the client. * @remarks * This class is used to send a series of updates to the client in a single response. The expected * sequence of calls is: * * `sendInformativeUpdate()`, `sendTextChunk()`, `sendTextChunk()`, ..., `endStream()`. * * Once `endStream()` is called, the stream is considered ended and no further updates can be sent. */ class StreamingResponse { _context; _nextSequence = 1; _streamId; _message = ''; _attachments; _ended = false; // Queue for outgoing activities _queue = []; _queueSync; _chunkQueued = false; // Powered by AI feature flags _enableFeedbackLoop = false; _feedbackLoopType; _enableGeneratedByAILabel = false; _citations = []; _sensitivityLabel; /** * Creates a new StreamingResponse instance. * @param {TurnContext} context - Context for the current turn of conversation with the user. * @returns {TurnContext} - The context for the current turn of conversation with the user. */ constructor(context) { this._context = context; } /** * Gets the stream ID of the current response. * @returns {string | undefined} - The stream ID of the current response. * @remarks * Assigned after the initial update is sent. */ get streamId() { return this._streamId; } /** * Gets the citations of the current response. */ get citations() { return this._citations; } /** * Gets the number of updates sent for the stream. * @returns {number} - The number of updates sent for the stream. */ get updatesSent() { return this._nextSequence - 1; } /** * Queues an informative update to be sent to the client. * @param {string} text Text of the update to send. */ queueInformativeUpdate(text) { if (this._ended) { throw new Error('The stream has already ended.'); } // Queue a typing activity this.queueActivity(() => ({ type: 'typing', text, channelData: { streamType: 'informative', streamSequence: this._nextSequence++ } })); } /** * Queues a chunk of partial message text to be sent to the client * @remarks * The text we be sent as quickly as possible to the client. Chunks may be combined before * delivery to the client. * @param {string} text Partial text of the message to send. * @param {Citation[]} citations Citations to be included in the message. */ queueTextChunk(text, citations) { if (this._ended) { throw new Error('The stream has already ended.'); } // Update full message text this._message += text; // If there are citations, modify the content so that the sources are numbers instead of [doc1], [doc2], etc. this._message = Utilities_1.Utilities.formatCitationsResponse(this._message); // Queue the next chunk this.queueNextChunk(); } /** * Ends the stream by sending the final message to the client. * @returns {Promise<void>} - A promise representing the async operation */ endStream() { if (this._ended) { throw new Error('The stream has already ended.'); } // Queue final message this._ended = true; this.queueNextChunk(); // Wait for the queue to drain return this.waitForQueue(); } /** * Sets the attachments to attach to the final chunk. * @param attachments List of attachments. */ setAttachments(attachments) { this._attachments = attachments; } /** * Sets the sensitivity label to attach to the final chunk. * @param sensitivityLabel The sensitivty label. */ setSensitivityLabel(sensitivityLabel) { this._sensitivityLabel = sensitivityLabel; } /** * Sets the citations for the full message. * @param {Citation[]} citations Citations to be included in the message. */ setCitations(citations) { if (citations.length > 0) { if (!this._citations) { this._citations = []; } let currPos = this._citations.length; for (const citation of citations) { const clientCitation = { '@type': 'Claim', position: currPos + 1, appearance: { '@type': 'DigitalDocument', name: citation.title || `Document #${currPos + 1}`, abstract: Utilities_1.Utilities.snippet(citation.content, 477) } }; currPos++; this._citations.push(clientCitation); } } } /** * Sets the Feedback Loop in Teams that allows a user to * give thumbs up or down to a response. * Default is `false`. * @param enableFeedbackLoop If true, the feedback loop is enabled. */ setFeedbackLoop(enableFeedbackLoop) { this._enableFeedbackLoop = enableFeedbackLoop; } /** * Sets the type of UI to use for the feedback loop. * @param feedbackLoopType The type of the feedback loop. */ setFeedbackLoopType(feedbackLoopType) { this._feedbackLoopType = feedbackLoopType; } /** * Sets the the Generated by AI label in Teams * Default is `false`. * @param enableGeneratedByAILabel If true, the label is added. */ setGeneratedByAILabel(enableGeneratedByAILabel) { this._enableGeneratedByAILabel = enableGeneratedByAILabel; } /** * Returns the most recently streamed message. * @returns The streamed message. */ getMessage() { return this._message; } /** * Waits for the outgoing activity queue to be empty. * @returns {Promise<void>} - A promise representing the async operation. */ waitForQueue() { return this._queueSync || Promise.resolve(); } /** * Queues the next chunk of text to be sent to the client. * @private */ queueNextChunk() { // Are we already waiting to send a chunk? if (this._chunkQueued) { return; } // Queue a chunk of text to be sent this._chunkQueued = true; this.queueActivity(() => { this._chunkQueued = false; if (this._ended) { // Send final message return { type: 'message', text: this._message, attachments: this._attachments, channelData: { streamType: 'final' } }; } else { // Send typing activity return { type: 'typing', text: this._message, channelData: { streamType: 'streaming', streamSequence: this._nextSequence++ } }; } }); } /** * Queues an activity to be sent to the client. * @param {() => Partial<Activity>} factory - A factory that creates the outgoing activity just before its sent. */ queueActivity(factory) { this._queue.push(factory); // If there's no sync in progress, start one if (!this._queueSync) { this._queueSync = this.drainQueue().catch((err) => { console.error(`Error occured when sending activity while streaming: "${err}".`); throw err; }); } } /** * Sends any queued activities to the client until the queue is empty. * @returns {Promise<void>} - A promise that will be resolved once the queue is empty. * @private */ drainQueue() { return new Promise(async (resolve, reject) => { try { while (this._queue.length > 0) { // Get next activity from queue const factory = this._queue.shift(); const activity = factory(); // Send activity await this.sendActivity(activity); } resolve(); } catch (err) { reject(err); } finally { // Queue is empty, mark as idle this._queueSync = undefined; } }); } /** * Sends an activity to the client and saves the stream ID returned. * @param {Partial<Activity>} activity - The activity to send. * @returns {Promise<void>} - A promise representing the async operation. * @private */ async sendActivity(activity) { // Set activity ID to the assigned stream ID if (this._streamId) { activity.id = this._streamId; activity.channelData = Object.assign({}, activity.channelData, { streamId: this._streamId }); } activity.entities = [ { type: 'streaminfo', ...activity.channelData } ]; if (this._citations && this._citations.length > 0 && !this._ended) { // Filter out the citations unused in content. const currCitations = Utilities_1.Utilities.getUsedCitations(this._message, this._citations) ?? undefined; activity.entities.push({ type: 'https://schema.org/Message', '@type': 'Message', '@context': 'https://schema.org', '@id': '', citation: currCitations }); } // Add in Powered by AI feature flags if (this._ended) { if (this._enableFeedbackLoop && this._feedbackLoopType) { activity.channelData = Object.assign({}, activity.channelData, { feedbackLoop: { type: this._feedbackLoopType } }); } else { activity.channelData = Object.assign({}, activity.channelData, { feedbackLoopEnabled: this._enableFeedbackLoop }); } // Add in Generated by AI if (this._enableGeneratedByAILabel) { activity.entities.push({ type: 'https://schema.org/Message', '@type': 'Message', '@context': 'https://schema.org', '@id': '', additionalType: ['AIGeneratedContent'], citation: this._citations && this._citations.length > 0 ? this._citations : [], usageInfo: this._sensitivityLabel }); } } // Send activity const response = await this._context.sendActivity(activity); await new Promise((resolve) => setTimeout(resolve, 1500)); // Save assigned stream ID if (!this._streamId) { this._streamId = response?.id; } } } exports.StreamingResponse = StreamingResponse; //# sourceMappingURL=StreamingResponse.js.map