UNPKG

@microsoft/teams-ai

Version:

SDK focused on building AI based applications for Microsoft Teams.

453 lines 15.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.TurnStateEntry = exports.TurnState = void 0; /** * @private */ const CONVERSATION_SCOPE = 'conversation'; /** * @private */ const USER_SCOPE = 'user'; /** * @private */ const TEMP_SCOPE = 'temp'; /** * Base class defining a collection of turn state scopes. * @remarks * Developers can create a derived class that extends `TurnState` to add additional state scopes. * ```JavaScript * class MyTurnState extends TurnState { * protected async onComputeStorageKeys(context) { * const keys = await super.onComputeStorageKeys(context); * keys['myScope'] = `myScopeKey`; * return keys; * } * * public get myScope() { * const scope = this.getScope('myScope'); * if (!scope) { * throw new Error(`MyTurnState hasn't been loaded. Call load() first.`); * } * return scope.value; * } * * public set myScope(value) { * const scope = this.getScope('myScope'); * if (!scope) { * throw new Error(`MyTurnState hasn't been loaded. Call load() first.`); * } * scope.replace(value); * } * } * ``` */ /** * Represents the turn state for a conversation. * Turn state includes conversation state, user state, and temporary state. * Provides methods to access, modify, and delete the state objects. * @template TConversationState * @param {TConversationState} TConversationState The type of the conversation state object. * @param {TUserState} TUserState The type of the user state object. * @param {TTempState} TTempState - The type of the temporary state object. */ class TurnState { _scopes = {}; _isLoaded = false; _loadingPromise; _stateNotLoadedString = `TurnState hasn't been loaded. Call load() first.`; /** * Accessor for the conversation state. */ /** * Gets the conversation state from the turn state. * @template TConversationState * @returns {TConversationState} The conversation state. * @throws Error if TurnState hasn't been loaded. Call load() first. */ get conversation() { const scope = this.getScope(CONVERSATION_SCOPE); if (!scope) { throw new Error(this._stateNotLoadedString); } return scope.value; } /** * Replaces the conversation state with a new value. * @param {TConversationState} value New value to replace the conversation state with. */ set conversation(value) { const scope = this.getScope(CONVERSATION_SCOPE); if (!scope) { throw new Error(this._stateNotLoadedString); } scope.replace(value); } /** * Gets a value indicating whether the applications turn state has been loaded. * @returns {boolean} True if the applications turn state has been loaded, false otherwise. */ get isLoaded() { return this._isLoaded; } /** * Accessor for the temp state. @returns {TTempState} The temp TurnState. @throws Error if TurnState hasn't been loaded. Call load() first. */ get temp() { const scope = this.getScope(TEMP_SCOPE); if (!scope) { throw new Error(this._stateNotLoadedString); } return scope.value; } /** * Replaces the temp state with a new value. * @param {TTempState} value New value to replace the temp state with. * @throws Error if TurnState hasn't been loaded. Call load() first. */ set temp(value) { const scope = this.getScope(TEMP_SCOPE); if (!scope) { throw new Error(this._stateNotLoadedString); } scope.replace(value); } /** * Accessor for the user state. * @returns {TUserState} The user TurnState. */ get user() { const scope = this.getScope(USER_SCOPE); if (!scope) { throw new Error(this._stateNotLoadedString); } return scope.value; } /** * Replaces the user state with a new value. */ set user(value) { const scope = this.getScope(USER_SCOPE); if (!scope) { throw new Error(this._stateNotLoadedString); } scope.replace(value); } /** * Deletes the state object for the current conversation from storage. */ deleteConversationState() { const scope = this.getScope(CONVERSATION_SCOPE); if (!scope) { throw new Error(this._stateNotLoadedString); } scope.delete(); } /** * Deletes the temp state object. */ deleteTempState() { const scope = this.getScope(TEMP_SCOPE); if (!scope) { throw new Error(this._stateNotLoadedString); } scope.delete(); } /** * Deletes the state object for the current user from storage. */ deleteUserState() { const scope = this.getScope(USER_SCOPE); if (!scope) { throw new Error(); } scope.delete(); } /** * Gets a state scope by name. * @param {string} scope Name of the state scope to return. (i.e. 'conversation', 'user', or 'temp') * @returns {string | undefined} The state scope or undefined if not found. */ getScope(scope) { return this._scopes[scope]; } /** * Deletes a value from the memory. * @param {string} path Path to the value to delete in the form of `[scope].property`. If scope is omitted, the value is deleted from the temporary scope. */ deleteValue(path) { const { scope, name } = this.getScopeAndName(path); if (scope.value.hasOwnProperty(name)) { delete scope.value[name]; } } /** * Checks if a value exists in the memory. * @param {string} path Path to the value to check in the form of `[scope].property`. If scope is omitted, the value is checked in the temporary scope. * @returns {boolean} True if the value exists, false otherwise. */ hasValue(path) { const { scope, name } = this.getScopeAndName(path); return Object.prototype.hasOwnProperty.call(scope.value, name); } /** * Retrieves a value from the memory. * @template TValue * @param {TValue} path Path to the value to retrieve in the form of `[scope].property`. If scope is omitted, the value is retrieved from the temporary scope. * @returns {string} The value or undefined if not found. */ getValue(path) { const { scope, name } = this.getScopeAndName(path); return scope.value[name]; } /** * Assigns a value to the memory. * @param {string} path Path to the value to assign in the form of `[scope].property`. If scope is omitted, the value is assigned to the temporary scope. * @param {unknown} value Value to assign. */ setValue(path, value) { const { scope, name } = this.getScopeAndName(path); scope.value[name] = value; } /** * Loads all of the state scopes for the current turn. * @param {TurnContext} context Context for the current turn of conversation with the user. * @param {Storage} storage Optional. Storage provider to load state scopes from. * @returns {boolean} True if the states needed to be loaded. */ load(context, storage) { // Only load on first call if (this._isLoaded) { return Promise.resolve(false); } // Check for existing load operation if (!this._loadingPromise) { this._loadingPromise = new Promise(async (resolve, reject) => { try { // Prevent additional load attempts this._isLoaded = true; // Compute state keys const keys = []; const scopes = await this.onComputeStorageKeys(context); for (const key in scopes) { if (Object.prototype.hasOwnProperty.call(scopes, key)) { keys.push(scopes[key]); } } // Read items from storage provider (if configured) const items = storage ? await storage.read(keys) : {}; // Create scopes for items for (const key in scopes) { if (Object.prototype.hasOwnProperty.call(scopes, key)) { const storageKey = scopes[key]; const value = items[storageKey]; this._scopes[key] = new TurnStateEntry(value, storageKey); } } // Add the temp scope this._scopes[TEMP_SCOPE] = new TurnStateEntry({}); // Clear loading promise this._isLoaded = true; this._loadingPromise = undefined; resolve(true); } catch (err) { this._loadingPromise = undefined; reject(err); } }); } return this._loadingPromise; } /** * Saves all of the state scopes for the current turn. * @param {TurnContext} context Context for the current turn of conversation with the user. * @param {Storage} storage Optional. Storage provider to save state scopes to. */ async save(context, storage) { // Check for existing load operation if (!this._isLoaded && this._loadingPromise) { // Wait for load to finish await this._loadingPromise; } // Ensure loaded if (!this._isLoaded) { throw new Error(this._stateNotLoadedString); } // Find changes and deletions let changes; let deletions; for (const key in this._scopes) { if (!Object.prototype.hasOwnProperty.call(this._scopes, key)) { continue; } const entry = this._scopes[key]; if (entry.storageKey) { if (entry.isDeleted) { // Add to deletion list if (deletions) { deletions.push(entry.storageKey); } else { deletions = [entry.storageKey]; } } else if (entry.hasChanged) { // Add to change set if (!changes) { changes = {}; } changes[entry.storageKey] = entry.value; } } } // Do we have a storage provider? if (storage) { // Apply changes const promises = []; if (changes) { promises.push(storage.write(changes)); } // Apply deletions if (deletions) { promises.push(storage.delete(deletions)); } // Wait for completion if (promises.length > 0) { await Promise.all(promises); } } } /** * Computes the storage keys for the state scopes being persisted. * @remarks * Can be overridden in derived classes to add additional storage scopes. * @param {TurnContext} context Context for the current turn of conversation with the user. * @returns {Promise<Record<string, string>>} A dictionary of scope names -> storage keys. * @throws Error if the context is missing a required property. */ onComputeStorageKeys(context) { // Compute state keys const activity = context.activity; const channelId = activity?.channelId; const botId = activity?.recipient?.id; const conversationId = activity?.conversation?.id; const userId = activity?.from?.id; if (!channelId) { throw new Error('missing context.activity.channelId'); } if (!botId) { throw new Error('missing context.activity.recipient.id'); } if (!conversationId) { throw new Error('missing context.activity.conversation.id'); } if (!userId) { throw new Error('missing context.activity.from.id'); } const keys = {}; keys[CONVERSATION_SCOPE] = `${channelId}/${botId}/conversations/${conversationId}`; keys[USER_SCOPE] = `${channelId}/${botId}/users/${userId}`; return Promise.resolve(keys); } /** * @private * @param {string} path Path to the value to check in the form of `[scope].property`. If scope is omitted, the value is checked in the temporary scope. * @returns {{ scope: TurnStateEntry; name: string }} Scope and name. */ getScopeAndName(path) { // Get variable scope and name const parts = path.split('.'); if (parts.length > 2) { throw new Error(`Invalid state path: ${path}`); } else if (parts.length === 1) { parts.unshift(TEMP_SCOPE); } // Validate scope const scope = this.getScope(parts[0]); if (scope === undefined) { throw new Error(`Invalid state scope: ${parts[0]}`); } return { scope, name: parts[1] }; } } exports.TurnState = TurnState; /** * Accessor class for managing an individual state scope. */ class TurnStateEntry { _value; _storageKey; _deleted = false; _hash; /** * Creates a new instance of the `TurnStateEntry` class. * @param {Record<string, unknown>} value Optional. Value to initialize the state scope with. The default is an {} object. * @param {string} storageKey Optional. Storage key to use when persisting the state scope. */ constructor(value, storageKey) { this._value = value || {}; this._storageKey = storageKey; this._hash = JSON.stringify(this._value); } /** * Gets a value indicating whether the state scope has changed since it was last loaded. * @returns {boolean} A value indicating whether the state scope has changed. */ get hasChanged() { return JSON.stringify(this._value) != this._hash; } /** * Gets a value indicating whether the state scope has been deleted. * @returns {boolean} A value indicating whether the state scope has been deleted. */ get isDeleted() { return this._deleted; } /** * Gets the value of the state scope. * @returns {Record<string, unknown>} value of the state scope. */ get value() { if (this.isDeleted) { // Switch to a replace scenario this._value = {}; this._deleted = false; } return this._value; } /** * Gets the storage key used to persist the state scope. * @returns {string | undefined} The storage key used to persist the state scope. */ get storageKey() { return this._storageKey; } /** * Clears the state scope. */ delete() { this._deleted = true; } /** * Replaces the state scope with a new value. * @template TValue * @param {TValue} value New value to replace the state scope with. */ replace(value) { this._value = value || {}; } } exports.TurnStateEntry = TurnStateEntry; //# sourceMappingURL=TurnState.js.map