UNPKG

@microsoft/botbuilder-m365

Version:

M365 extensions for Microsoft BotBuilder, Alpha release.

284 lines (251 loc) 11.2 kB
/** * @module botbuilder-m365 */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ // TODO: /* eslint-disable security/detect-object-injection */ import { encode } from 'gpt-3-encoder'; import { TurnState } from './TurnState'; export class ConversationHistory { /** * Name of the conversation state property used to hold the list of entires. */ public static readonly StatePropertyName = '__history__'; /** * Adds a new line of text to conversation history * * @param {TurnState} state Applications turn state. * @param {string} line Line of text to add to history. * @param {number} maxLines Optional. Maximum number of lines to store. Defaults to 10. */ public static addLine(state: TurnState, line: string, maxLines = 10): void { if (state.conversation) { // Create history array if it doesn't exist let history: string[] = state.conversation.value[ConversationHistory.StatePropertyName]; if (!Array.isArray(history)) { history = []; } // Add line to history history.push(line); // Prune history if too long if (history.length > maxLines) { history.splice(0, history.length - maxLines); } // Save history back to conversation state state.conversation.value[ConversationHistory.StatePropertyName] = history; } else { throw new Error( `ConversationHistory.addLine() was passed a state object without a 'conversation' state member.` ); } } public static appendToLastLine(state: TurnState, text: string): void { const line = ConversationHistory.getLastLine(state); ConversationHistory.replaceLastLine(state, line + text); } public static clear(state: TurnState): void { if (state.conversation) { state.conversation.value[ConversationHistory.StatePropertyName] = []; } else { throw new Error( `ConversationHistory.clear() was passed a state object without a 'conversation' state member.` ); } } public static hasMoreLines(state: TurnState): boolean { if (state.conversation) { const history: string[] = state.conversation.value[ConversationHistory.StatePropertyName]; return Array.isArray(history) ? history.length > 0 : false; } else { throw new Error( `ConversationHistory.hasMoreLines() was passed a state object without a 'conversation' state member.` ); } } public static getLastLine(state: TurnState): string { if (state.conversation) { // Create history array if it doesn't exist let history: string[] = state.conversation.value[ConversationHistory.StatePropertyName]; if (!Array.isArray(history)) { history = []; } // Return the last line or an empty string return history.length > 0 ? history[history.length - 1] : ''; } else { throw new Error( `ConversationHistory.getLastLine() was passed a state object without a 'conversation' state member.` ); } } public static getLastSay(state: TurnState): string { // Find start of text const lastLine = this.getLastLine(state); let textPos = lastLine.lastIndexOf(' SAY '); if (textPos >= 0) { textPos += 5; } else { // Find end of prefix textPos = lastLine.indexOf(': '); if (textPos >= 0) { textPos += 2; } else { // Just use whole text textPos = 0; } } // Trim off any DO statements let text = lastLine.substring(textPos); let doPos = text.indexOf(' THEN DO '); if (doPos >= 0) { text = text.substring(0, doPos); } else { doPos = text.indexOf(' DO '); if (doPos >= 0) { text = text.substring(0, doPos); } } return text.indexOf('DO ') < 0 ? text.trim() : ''; } public static removeLastLine(state: TurnState): string | undefined { if (state.conversation) { // Create history array if it doesn't exist let history: string[] = state.conversation.value[ConversationHistory.StatePropertyName]; if (!Array.isArray(history)) { history = []; } // Remove last line // - undefined is returned if the array is empty const line = history.pop(); // Save history back to conversation state state.conversation.value[ConversationHistory.StatePropertyName] = history; // Return removed line return line; } else { throw new Error( `ConversationHistory.removeLastLine() was passed a state object without a 'conversation' state member.` ); } } public static replaceLastLine(state: TurnState, line: string): void { if (state.conversation) { // Create history array if it doesn't exist let history: string[] = state.conversation.value[ConversationHistory.StatePropertyName]; if (!Array.isArray(history)) { history = []; } // Replace the last line or add a new one if (history.length > 0) { history[history.length - 1] = line; } else { history.push(line); } // Save history back to conversation state state.conversation.value[ConversationHistory.StatePropertyName] = history; } else { throw new Error( `ConversationHistory.replaceLastLine() was passed a state object without a 'conversation' state member.` ); } } public static replaceLastSay(state: TurnState, newResponse: string, botPrefix = 'AI: '): void { if (state.conversation) { // Create history array if it doesn't exist let history: string[] = state.conversation.value[ConversationHistory.StatePropertyName]; if (!Array.isArray(history)) { history = []; } // Update the last line or add a new one if (history.length > 0) { const line = history[history.length - 1]; const lastSayPos = line.lastIndexOf(' SAY '); if (lastSayPos >= 0 && line.indexOf(' DO ', lastSayPos) < 0) { // We found the last SAY and it wasn't followed by a DO history[history.length - 1] = `${line.substring(0, lastSayPos)} SAY ${newResponse}`; } else if (line.indexOf(' DO ') >= 0) { // Append a THEN SAY after the DO's history[history.length - 1] = `${line} THEN SAY ${newResponse}`; } else { // Just replace the entire line history[history.length - 1] = `${botPrefix}${newResponse}`; } } else { history.push(`${botPrefix}${newResponse}`); } // Save history back to conversation state state.conversation.value[ConversationHistory.StatePropertyName] = history; } else { throw new Error( `ConversationHistory.replaceLastSay() was passed a state object without a 'conversation' state member.` ); } } /** * Returns the current conversation history as a string of text. * * * The length of the returned text is gated by `maxCharacterLength` and only whole lines of * history entries will be returned. That means that if the length of the most recent history * entry is greater then `maxCharacterLength` no text will be returned. * * @param {TurnState} state Applications turn state. * @param {number} maxTokens Optional. Maximum length of the text returned. Defaults to 1000 tokens. * @param {string} lineSeparator Optional. Separator used between lines. Defaults to '\n'. * @returns {string} The most recent lines of conversation history as a text string. */ public static toString(state: TurnState, maxTokens = 1000, lineSeparator = '\n'): string { if (state.conversation) { // Get history array if it exists const history: string[] = state.conversation.value[ConversationHistory.StatePropertyName] ?? []; // Populate up to max chars let text = ''; let textTokens = 0; const lineSeparatorTokens = encode(lineSeparator).length; for (let i = history.length - 1; i >= 0; i--) { // Ensure that adding line won't go over the max character length const line = history[i]; const lineTokens = encode(line).length; const newTextTokens = textTokens + lineTokens + lineSeparatorTokens; if (newTextTokens > maxTokens) { break; } // Prepend line to output text = `${line}${lineSeparator}${text}`; textTokens = newTextTokens; } return text.trim(); } else { throw new Error( `ConversationHistory.toString() was passed a state object without a 'conversation' state member.` ); } } public static toArray(state: TurnState, maxTokens = 1000): string[] { if (state.conversation) { // Get history array if it exists const history: string[] = state.conversation.value[ConversationHistory.StatePropertyName] ?? []; // Populate up to max chars let textTokens = 0; const lines: string[] = []; for (let i = history.length - 1; i >= 0; i--) { // Ensure that adding line won't go over the max character length const line = history[i]; const lineTokens = encode(line).length; const newTextTokens = textTokens + lineTokens; if (newTextTokens > maxTokens) { break; } // Prepend line to output textTokens = newTextTokens; lines.unshift(line); } return lines; } else { throw new Error( `ConversationHistory.toArray() was passed a state object without a 'conversation' state member.` ); } } }