@microsoft/botbuilder-m365
Version:
M365 extensions for Microsoft BotBuilder, Alpha release.
174 lines • 8.22 kB
JavaScript
;
/* eslint-disable security/detect-object-injection */
/**
* @module botbuilder-m365
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PromptTemplateEngine = void 0;
const Block_1 = require("./Block");
const CodeBlock_1 = require("./CodeBlock");
const TextBlock_1 = require("./TextBlock");
const VarBlock_1 = require("./VarBlock");
/**
* Given a prompt, that might contain references to variables and functions:
* - Get the list of references
* - Resolve each reference
* - Variable references are resolved using the context variables
* - Function references are resolved invoking those functions
* - Functions can be invoked passing in variables
* - Functions do not receive the context variables, unless specified using a special variable
* - Functions can be invoked in order and in parallel so the context variables must be immutable when invoked within the template
*/
class PromptTemplateEngine {
constructor(promptManager) {
// Blocks delimitation
this.starter = '{';
this.ender = '}';
this._promptManager = promptManager;
}
extractBlocks(templateText, validate = true) {
const blocks = this.tokenizeInternal(templateText);
if (validate) {
this.validateBlocksSyntax(blocks);
}
return blocks;
}
render(context, state, textOrBlocks) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof textOrBlocks == 'string') {
const blocks = this.extractBlocks(textOrBlocks);
return yield this.render(context, state, blocks);
}
else {
let result = '';
for (const block of textOrBlocks) {
switch (block.type) {
case Block_1.BlockTypes.Text:
result += block.content;
break;
case Block_1.BlockTypes.Variable:
result += block.render(context, state);
break;
case Block_1.BlockTypes.Code:
result += yield block.renderCode(context, state, this._promptManager);
break;
case Block_1.BlockTypes.Undefined:
default:
throw new Error(`Invalid bock of type "${block.type}" encountered.`);
}
}
// TODO: good time to count tokens, though that depends on the model used
return result;
}
});
}
renderVariables(context, state, blocks) {
return blocks.map((block) => {
return block.type != Block_1.BlockTypes.Variable ? block : new TextBlock_1.TextBlock(block.render(context, state));
});
}
renderCode(context, state, blocks) {
return __awaiter(this, void 0, void 0, function* () {
const updatedBlocks = [];
for (const block of blocks) {
if (block.type != Block_1.BlockTypes.Code) {
updatedBlocks.push(block);
}
else {
const codeResult = yield block.renderCode(context, state, this._promptManager);
updatedBlocks.push(new TextBlock_1.TextBlock(codeResult));
}
}
return updatedBlocks;
});
}
tokenizeInternal(template) {
// An empty block consists of 4 chars: "{{}}"
const EMPTY_CODE_BLOCK_LENGTH = 4;
// A block shorter than 5 chars is either empty or invalid, e.g. "{{ }}" and "{{$}}"
const MIN_CODE_BLOCK_LENGTH = EMPTY_CODE_BLOCK_LENGTH + 1;
// Render NULL to ""
// Since the template is a string, all `template` instances can have non-null assertion
if (template === null) {
return [new TextBlock_1.TextBlock('')];
}
// If the template is "empty" return the content as a text block
if (template.length < MIN_CODE_BLOCK_LENGTH) {
return [new TextBlock_1.TextBlock(template)];
}
const blocks = [];
let cursor = 0;
let endOfLastBlock = 0;
let startPos = 0;
let startFound = false;
while (cursor < template.length - 1) {
// When "{{" is found
if (template[cursor] === this.starter && template[cursor + 1] === this.starter) {
startPos = cursor;
startFound = true;
}
// When "}}" is found
else if (startFound && template[cursor] === this.ender && template[cursor + 1] === this.ender) {
// If there is plain text between the current var/code block and the previous one, capture that as a TextBlock
if (startPos > endOfLastBlock) {
blocks.push(new TextBlock_1.TextBlock(template, endOfLastBlock, startPos));
}
// Skip ahead to the second "}" of "}}"
cursor++;
// Extract raw block
const contentWithDelimiters = template.substring(startPos, cursor + 1);
// Remove "{{" and "}}" delimiters and trim empty chars
const contentWithoutDelimiters = contentWithDelimiters
.substring(2, contentWithDelimiters.length - 2)
.trim();
if (contentWithoutDelimiters.length === 0) {
// If what is left is empty, consider the raw block a Text Block
blocks.push(new TextBlock_1.TextBlock(contentWithDelimiters));
}
else {
// If the block starts with "$" it's a variable
if (VarBlock_1.VarBlock.hasVarPrefix(contentWithoutDelimiters)) {
// Note: validation is delayed to the time VarBlock is rendered
blocks.push(new VarBlock_1.VarBlock(contentWithoutDelimiters));
}
else {
// Note: validation is delayed to the time CodeBlock is rendered
blocks.push(new CodeBlock_1.CodeBlock(contentWithoutDelimiters));
}
}
endOfLastBlock = cursor + 1;
startFound = false;
}
cursor++;
}
// If there is something left after the last block, capture it as a TextBlock
if (endOfLastBlock < template.length) {
blocks.push(new TextBlock_1.TextBlock(template, endOfLastBlock, template.length));
}
return blocks;
}
validateBlocksSyntax(blocks) {
blocks.forEach((block) => {
var _a;
const { valid, errorMessage: error } = block.isValid();
if (!valid || error) {
throw new Error(`Prompt template syntax error: ${(_a = error === null || error === void 0 ? void 0 : error.toString()) !== null && _a !== void 0 ? _a : 'unknown'}`);
}
});
}
}
exports.PromptTemplateEngine = PromptTemplateEngine;
//# sourceMappingURL=PromptTemplateEngine.js.map