UNPKG

@microsoft/botbuilder-m365

Version:

M365 extensions for Microsoft BotBuilder, Alpha release.

169 lines (144 loc) 6.61 kB
/** * @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[]; }