@microsoft/teams-ai
Version:
SDK focused on building AI based applications for Microsoft Teams.
346 lines • 14.7 kB
JavaScript
;
/**
* @module teams-ai
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PromptManager = void 0;
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const augmentations_1 = require("../augmentations");
const ActionOutputMessage_1 = require("./ActionOutputMessage");
const ConversationHistory_1 = require("./ConversationHistory");
const DataSourceSection_1 = require("./DataSourceSection");
const GroupSection_1 = require("./GroupSection");
const Prompt_1 = require("./Prompt");
const TemplateSection_1 = require("./TemplateSection");
const UserInputMessage_1 = require("./UserInputMessage");
const UserMessage_1 = require("./UserMessage");
/**
* A filesystem based prompt manager.
* @remarks
* The default prompt manager uses the file system to define prompts that are compatible with
* Microsoft's Semantic Kernel SDK (see: https://github.com/microsoft/semantic-kernel)
*
* Each prompt is a separate folder under a root prompts folder. The folder should contain the following files:
*
* - "config.json": Required. Contains the prompts configuration and is a serialized instance of `PromptTemplateConfig`.
* - "skprompt.txt": Required. Contains the text of the prompt and supports Semantic Kernels prompt template syntax.
* - "actions.json": Optional. Contains a list of actions that can be called by the prompt.
*
* Prompts can be loaded and used by name and new dynamically defined prompt templates can be
* registered with the prompt manager.
* @template TState Optional. Type of the applications turn state.
*/
class PromptManager {
_options;
_dataSources = new Map();
_functions = new Map();
_prompts = new Map();
/**
* Creates a new 'PromptManager' instance.
* @param {PromptManagerOptions} options - Options used to configure the prompt manager.
* @returns {PromptManager} A new prompt manager instance.
*/
constructor(options) {
this._options = Object.assign({
role: 'system',
max_conversation_history_tokens: 1.0,
max_history_messages: 10,
max_input_tokens: -1
}, options);
}
/**
* Gets the configured prompt manager options.
* @returns {ConfiguredPromptManagerOptions} The configured prompt manager options.
*/
get options() {
return this._options;
}
/**
* Registers a new data source with the prompt manager.
* @param {DataSource} dataSource - Data source to add.
* @returns {this} The prompt manager for chaining.
*/
addDataSource(dataSource) {
if (this._dataSources.has(dataSource.name)) {
throw new Error(`DataSource '${dataSource.name}' already exists.`);
}
this._dataSources.set(dataSource.name, dataSource);
return this;
}
/**
* Looks up a data source by name.
* @param {string} name - Name of the data source to lookup.
* @returns {DataSource} The data source.
*/
getDataSource(name) {
const dataSource = this._dataSources.get(name);
if (!dataSource) {
throw new Error(`DataSource '${name}' not found.`);
}
return dataSource;
}
/**
* Checks for the existence of a named data source.
* @param {string} name - Name of the data source to lookup.
* @returns {boolean} True if the data source exists.
*/
hasDataSource(name) {
return this._dataSources.has(name);
}
/**
* Registers a new prompt template function with the prompt manager.
* @param {string} name - Name of the function to add.
* @param {PromptFunction} fn - Function to add.
* @returns {this} - The prompt manager for chaining.
*/
addFunction(name, fn) {
if (this._functions.has(name)) {
throw new Error(`Function '${name}' already exists.`);
}
this._functions.set(name, fn);
return this;
}
/**
* Looks up a prompt template function by name.
* @param {string} name - Name of the function to lookup.
* @returns {PromptFunction} The function.
*/
getFunction(name) {
const fn = this._functions.get(name);
if (!fn) {
throw new Error(`Function '${name}' not found.`);
}
return fn;
}
/**
* Checks for the existence of a named prompt template function.
* @param {string} name Name of the function to lookup.
* @returns {boolean} True if the function exists.
*/
hasFunction(name) {
return this._functions.has(name);
}
/**
* Invokes a prompt template function by name.
* @param {string} name - Name of the function to invoke.
* @param {TurnContext} context - Turn context for the current turn of conversation with the user.
* @param {Memory} memory - An interface for accessing state values.
* @param {Tokenizer} tokenizer - Tokenizer to use when rendering the prompt.
* @param {string[]} args - Arguments to pass to the function.
* @returns {Promise<any>} Value returned by the function.
*/
invokeFunction(name, context, memory, tokenizer, args) {
const fn = this.getFunction(name);
return fn(context, memory, this, tokenizer, args);
}
/**
* Registers a new prompt template with the prompt manager.
* @param {PromptTemplate} prompt - Prompt template to add.
* @returns {this} The prompt manager for chaining.
*/
addPrompt(prompt) {
if (this._prompts.has(prompt.name)) {
throw new Error(`The PromptManager.addPrompt() method was called with a previously registered prompt named "${prompt.name}".`);
}
// Clone and cache prompt
const clone = Object.assign({}, prompt);
this._prompts.set(prompt.name, clone);
return this;
}
/**
* Loads a named prompt template from the filesystem.
* @remarks
* The template will be pre-parsed and cached for use when the template is rendered by name.
*
* Any augmentations will also be added to the template.
* @param {string} name - Name of the prompt to load.
* @returns {Promise<PromptTemplate>} The loaded and parsed prompt template.
*/
async getPrompt(name) {
if (!this._prompts.has(name)) {
const template = { name };
// Load template from disk
const folder = path.join(this._options.promptsFolder, name);
const configFile = path.join(folder, 'config.json');
const promptFile = path.join(folder, 'skprompt.txt');
const actionsFile = path.join(folder, 'actions.json');
// Load prompt config
try {
const config = await fs.readFile(configFile, 'utf-8');
template.config = JSON.parse(config);
}
catch (err) {
throw new Error(`PromptManager.getPrompt(): an error occurred while loading '${configFile}'. The file is either invalid or missing.`);
}
// Load prompt text
let sections = [];
try {
const role = this._options.role || 'system';
const prompt = await fs.readFile(promptFile, 'utf-8');
sections.push(new TemplateSection_1.TemplateSection(prompt, role));
}
catch (err) {
throw new Error(`PromptManager.getPrompt(): an error occurred while loading '${promptFile}'. The file is either invalid or missing.`);
}
// Load optional actions
try {
const actions = await fs.readFile(actionsFile, 'utf-8');
template.actions = JSON.parse(actions);
}
catch (err) {
// Ignore missing actions file
}
// Update the templates config file with defaults and migrate as needed
this.updateConfig(template);
// Add augmentations
this.appendAugmentations(template, sections);
// Group everything into a system message
sections = [new GroupSection_1.GroupSection(sections, 'system')];
// Include conversation history
// - The ConversationHistory section will use the remaining tokens from
// max_input_tokens.
if (template.config.completion.include_history) {
sections.push(new ConversationHistory_1.ConversationHistory(`conversation.${template.name}_history`, this.options.max_conversation_history_tokens));
}
if (template.config.augmentation && template.config.augmentation.augmentation_type === 'tools') {
const includeHistory = template.config.completion.include_history;
const historyVariable = includeHistory ? `conversation.${name}_history` : 'temp.${name}_history';
sections.push(new ActionOutputMessage_1.ActionOutputMessage(historyVariable));
}
// Include user input
if (template.config.completion.include_images) {
sections.push(new UserInputMessage_1.UserInputMessage(this.options.max_input_tokens));
}
else if (template.config.completion.include_input) {
sections.push(new UserMessage_1.UserMessage('{{$temp.input}}', this.options.max_input_tokens));
}
// Create prompt
template.prompt = new Prompt_1.Prompt(sections);
// Cache loaded template
this._prompts.set(name, template);
}
return this._prompts.get(name);
}
/**
* Checks for the existence of a named prompt.
* @param {string} name - Name of the prompt to load.
* @returns {boolean} True if the prompt exists.
*/
async hasPrompt(name) {
if (!this._prompts.has(name)) {
const folder = path.join(this._options.promptsFolder, name);
const promptFile = path.join(folder, 'skprompt.txt');
// Check for prompt existence
try {
await fs.access(promptFile);
}
catch (err) {
return false;
}
}
return true;
}
/**
* @param {PromptTemplate} template - The prompt template to update.
* @private
*/
updateConfig(template) {
// Set config defaults
template.config.completion = Object.assign({
frequency_penalty: 0.0,
include_history: true,
include_input: true,
include_images: false,
max_tokens: 150,
max_input_tokens: 2048,
presence_penalty: 0.0,
temperature: 0.0,
top_p: 0.0
}, template.config.completion);
// Migrate old schema
if (template.config.schema === 1) {
template.config.schema = 1.1;
if (Array.isArray(template.config.default_backends) && template.config.default_backends.length > 0) {
template.config.completion.model = template.config.default_backends[0];
}
}
}
/**
* @param {PromptTemplate} template - The prompt template to append augmentations to.
* @param {PromptSection[]} sections - The prompt sections to append augmentations to.
* @private
*/
appendAugmentations(template, sections) {
// Check for augmentation
const augmentation = template.config.augmentation;
if (augmentation) {
// First append data sources
// - We're using a minimum of 2 tokens for each data source to prevent
// any sort of prompt rendering conflicts between sources and conversation history.
// - If we wanted to let users specify a percentage% for a data source we would need
// to track the percentage they gave the data source(s) and give the remaining to
// the ConversationHistory section.
const data_sources = augmentation.data_sources ?? {};
for (const name in data_sources) {
if (!this.hasDataSource(name)) {
throw new Error(`DataSource '${name}' not found for prompt '${template.name}'.`);
}
const dataSource = this.getDataSource(name);
const tokens = Math.max(data_sources[name], 2);
sections.push(new DataSourceSection_1.DataSourceSection(dataSource, tokens));
}
// Next create augmentation
switch (augmentation.augmentation_type) {
default:
case 'none':
// No augmentation needed
break;
case 'monologue':
template.augmentation = new augmentations_1.MonologueAugmentation(template.actions ?? []);
break;
case 'sequence':
template.augmentation = new augmentations_1.SequenceAugmentation(template.actions ?? []);
break;
case 'tools':
template.augmentation = new augmentations_1.ToolsAugmentation();
}
// Append the augmentations prompt section
if (template.augmentation) {
const section = template.augmentation.createPromptSection();
if (section) {
sections.push(section);
}
}
}
}
}
exports.PromptManager = PromptManager;
//# sourceMappingURL=PromptManager.js.map