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