@microsoft/teams-ai
Version:
SDK focused on building AI based applications for Microsoft Teams.
338 lines • 11.7 kB
JavaScript
"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