@microsoft/botbuilder-m365
Version:
M365 extensions for Microsoft BotBuilder, Alpha release.
169 lines (144 loc) • 6.61 kB
text/typescript
/**
* @module botbuilder-m365
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { TurnContext } from 'botbuilder';
import * as fs from 'fs/promises';
import * as path from 'path';
import { DefaultTurnState } from './DefaultTurnStateManager';
import { PromptManager, PromptTemplate } from './Prompts';
import { Block, PromptTemplateEngine } from './PromptTemplateEngine';
import { TurnState } from './TurnState';
export interface DefaultPromptManagerOptions {
promptsFolder: string;
}
export class DefaultPromptManager<TState extends TurnState = DefaultTurnState> implements PromptManager<TState> {
private readonly _functions: Map<string, TemplateFunctionEntry<TState>> = new Map();
private readonly _templates: Map<string, CachedPromptTemplate> = new Map();
private readonly _options: DefaultPromptManagerOptions;
private readonly _templateEngine: PromptTemplateEngine<TState>;
public constructor(options: DefaultPromptManagerOptions | string) {
this._options = typeof options == 'object' ? Object.assign({}, options) : { promptsFolder: options };
this._templateEngine = new PromptTemplateEngine(this);
}
public addFunction(
name: string,
handler: (context: TurnContext, state: TState) => Promise<any>,
allowOverrides = false
): this {
if (!this._functions.has(name) || allowOverrides) {
this._functions.set(name, { handler, allowOverrides });
} else {
const entry = this._functions.get(name);
if (entry!.allowOverrides) {
entry!.handler = handler;
} else {
throw new Error(
`The DefaultPromptManager.templateFunction() method was called with a previously registered function named "${name}".`
);
}
}
return this;
}
public addPromptTemplate(name: string, template: PromptTemplate): this {
if (this._templates.has(name)) {
throw new Error(
`The DefaultPromptManager.addPromptTemplate() method was called with a previously registered template named "${name}".`
);
}
const entry = Object.assign({}, template) as CachedPromptTemplate;
// Parse prompt into blocks
try {
entry.blocks = this._templateEngine.extractBlocks(entry.text, true);
} catch (err: unknown) {
throw new Error(
`DefaultPromptManager.addPromptTemplate(): an error occurred while parsing the template for '${name}': ${(
err as Error
).toString()}`
);
}
// Cache template
this._templates.set(name, entry);
return this;
}
public invokeFunction(context: TurnContext, state: TState, name: string): Promise<any> {
if (this._functions && this._functions.has(name)) {
return Promise.resolve(this._functions.get(name)?.handler(context, state));
} else {
throw new Error(
`The DefaultPromptManager.invokeFunction() method was called for an unregistered function named "${name}".`
);
}
}
public async loadPromptTemplate(name: string): Promise<PromptTemplate> {
if (!this._templates.has(name)) {
const entry = {} as CachedPromptTemplate;
// 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');
// Load prompt config
try {
// eslint-disable-next-line security/detect-non-literal-fs-filename
const config = await fs.readFile(configFile, 'utf-8');
entry.config = JSON.parse(config);
} catch (err: unknown) {
throw new Error(
`DefaultPromptManager.loadPromptTemplate(): an error occurred while loading '${configFile}'. The file is either invalid or missing.`
);
}
// Load prompt text
try {
// eslint-disable-next-line security/detect-non-literal-fs-filename
entry.text = await fs.readFile(promptFile, 'utf-8');
} catch (err: unknown) {
throw new Error(
`DefaultPromptManager.loadPromptTemplate(): an error occurred while loading '${promptFile}'. The file is either invalid or missing.`
);
}
// Parse prompt into blocks
try {
entry.blocks = this._templateEngine.extractBlocks(entry.text, true);
} catch (err: unknown) {
throw new Error(
`DefaultPromptManager.loadPromptTemplate(): an error occurred while parsing '${promptFile}': ${(
err as Error
).toString()}`
);
}
// Cache loaded template
this._templates.set(name, entry);
}
return this._templates.get(name) || ({} as CachedPromptTemplate);
}
public async renderPrompt(
context: TurnContext,
state: TState,
nameOrTemplate: string | PromptTemplate
): Promise<PromptTemplate> {
// Load the template if needed
let template: CachedPromptTemplate;
if (typeof nameOrTemplate == 'string') {
template = (await this.loadPromptTemplate(nameOrTemplate)) as CachedPromptTemplate;
} else if (typeof nameOrTemplate == 'object' && nameOrTemplate.text && nameOrTemplate.config) {
template = Object.assign({}, nameOrTemplate) as CachedPromptTemplate;
} else {
throw new Error(
`The DefaultPromptManager.renderPrompt() method was passed an invalid or missing template.`
);
}
// Render the prompt
const text = await this._templateEngine.render(context, state, template.blocks ?? template.text);
return { text: text, config: template.config };
}
}
interface TemplateFunctionEntry<TState> {
handler: (context: TurnContext, state: TState) => Promise<any>;
allowOverrides: boolean;
}
interface CachedPromptTemplate extends PromptTemplate {
blocks: Block[];
}