UNPKG

@promptbook/core

Version:

It's time for a paradigm shift. The future of software in plain English, French or Latin

1,347 lines (1,277 loc) โ€ข 471 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('spacetrim'), require('prettier'), require('prettier/parser-html'), require('rxjs'), require('crypto'), require('waitasecond'), require('papaparse'), require('crypto-js/enc-hex'), require('crypto-js/sha256'), require('path'), require('crypto-js'), require('mime-types'), require('moment'), require('colors')) : typeof define === 'function' && define.amd ? define(['exports', 'spacetrim', 'prettier', 'prettier/parser-html', 'rxjs', 'crypto', 'waitasecond', 'papaparse', 'crypto-js/enc-hex', 'crypto-js/sha256', 'path', 'crypto-js', 'mime-types', 'moment', 'colors'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["promptbook-core"] = {}, global.spaceTrim, global.prettier, global.parserHtml, global.rxjs, global.crypto, global.waitasecond, global.papaparse, global.hexEncoder, global.sha256, global.path, global.cryptoJs, global.mimeTypes, global.moment, global.colors)); })(this, (function (exports, spaceTrim, prettier, parserHtml, rxjs, crypto, waitasecond, papaparse, hexEncoder, sha256, path, cryptoJs, mimeTypes, moment, colors) { 'use strict'; function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var spaceTrim__default = /*#__PURE__*/_interopDefaultLegacy(spaceTrim); var parserHtml__default = /*#__PURE__*/_interopDefaultLegacy(parserHtml); var hexEncoder__default = /*#__PURE__*/_interopDefaultLegacy(hexEncoder); var sha256__default = /*#__PURE__*/_interopDefaultLegacy(sha256); var moment__default = /*#__PURE__*/_interopDefaultLegacy(moment); var colors__default = /*#__PURE__*/_interopDefaultLegacy(colors); // โš ๏ธ WARNING: This code has been generated so that any manual changes will be overwritten /** * The version of the Book language * * @generated * @see https://github.com/webgptorg/book */ const BOOK_LANGUAGE_VERSION = '1.0.0'; /** * The version of the Promptbook engine * * @generated * @see https://github.com/webgptorg/promptbook */ const PROMPTBOOK_ENGINE_VERSION = '0.89.0-16'; /** * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name */ /** * Converts PipelineCollection to serialized JSON * * Note: Functions `collectionToJson` and `createCollectionFromJson` are complementary * * @public exported from `@promptbook/core` */ async function collectionToJson(collection) { const pipelineUrls = await collection.listPipelines(); const promptbooks = await Promise.all(pipelineUrls.map((url) => collection.getPipelineByUrl(url))); return promptbooks; } /** * TODO: [๐Ÿง ] Maybe clear `sourceFile` or clear when exposing through API or remote server */ /** * Checks if value is valid email * * @public exported from `@promptbook/utils` */ function isValidEmail(email) { if (typeof email !== 'string') { return false; } if (email.split('\n').length > 1) { return false; } return /^.+@.+\..+$/.test(email); } /** * Tests if given string is valid URL. * * Note: This does not check if the file exists only if the path is valid * @public exported from `@promptbook/utils` */ function isValidFilePath(filename) { if (typeof filename !== 'string') { return false; } if (filename.split('\n').length > 1) { return false; } if (filename.split(' ').length > 5 /* <- TODO: [๐Ÿง ][๐Ÿˆท] Make some better non-arbitrary way how to distinct filenames from informational texts */) { return false; } const filenameSlashes = filename.split('\\').join('/'); // Absolute Unix path: /hello.txt if (/^(\/)/i.test(filenameSlashes)) { // console.log(filename, 'Absolute Unix path: /hello.txt'); return true; } // Absolute Windows path: /hello.txt if (/^([A-Z]{1,2}:\/?)\//i.test(filenameSlashes)) { // console.log(filename, 'Absolute Windows path: /hello.txt'); return true; } // Relative path: ./hello.txt if (/^(\.\.?\/)+/i.test(filenameSlashes)) { // console.log(filename, 'Relative path: ./hello.txt'); return true; } // Allow paths like foo/hello if (/^[^/]+\/[^/]+/i.test(filenameSlashes)) { // console.log(filename, 'Allow paths like foo/hello'); return true; } // Allow paths like hello.book if (/^[^/]+\.[^/]+$/i.test(filenameSlashes)) { // console.log(filename, 'Allow paths like hello.book'); return true; } return false; } /** * TODO: [๐Ÿ] Implement for MacOs */ /** * Tests if given string is valid URL. * * Note: Dataurl are considered perfectly valid. * Note: There are two simmilar functions: * - `isValidUrl` which tests any URL * - `isValidPipelineUrl` *(this one)* which tests just promptbook URL * * @public exported from `@promptbook/utils` */ function isValidUrl(url) { if (typeof url !== 'string') { return false; } try { if (url.startsWith('blob:')) { url = url.replace(/^blob:/, ''); } const urlObject = new URL(url /* because fail is handled */); if (!['http:', 'https:', 'data:'].includes(urlObject.protocol)) { return false; } return true; } catch (error) { return false; } } /** * This error indicates that the promptbook in a markdown format cannot be parsed into a valid promptbook object * * @public exported from `@promptbook/core` */ class ParseError extends Error { constructor(message) { super(message); this.name = 'ParseError'; Object.setPrototypeOf(this, ParseError.prototype); } } /** * TODO: Maybe split `ParseError` and `ApplyError` */ /** * Returns the same value that is passed as argument. * No side effects. * * Note: It can be usefull for: * * 1) Leveling indentation * 2) Putting always-true or always-false conditions without getting eslint errors * * @param value any values * @returns the same values * @private within the repository */ function just(value) { if (value === undefined) { return undefined; } return value; } /** * Warning message for the generated sections and files files * * @private within the repository */ const GENERATOR_WARNING = `โš ๏ธ WARNING: This code has been generated so that any manual changes will be overwritten`; /** * Name for the Promptbook * * TODO: [๐Ÿ—ฝ] Unite branding and make single place for it * * @public exported from `@promptbook/core` */ const NAME = `Promptbook`; /** * Email of the responsible person * * @public exported from `@promptbook/core` */ const ADMIN_EMAIL = 'pavol@ptbk.io'; /** * Name of the responsible person for the Promptbook on GitHub * * @public exported from `@promptbook/core` */ const ADMIN_GITHUB_NAME = 'hejny'; /** * Claim for the Promptbook * * TODO: [๐Ÿ—ฝ] Unite branding and make single place for it * * @public exported from `@promptbook/core` */ const CLAIM = `It's time for a paradigm shift. The future of software in plain English, French or Latin`; // <- TODO: [๐ŸŠ] Pick the best claim /** * When the title is not provided, the default title is used * * @public exported from `@promptbook/core` */ const DEFAULT_BOOK_TITLE = `โœจ Untitled Book`; /** * When the title of task is not provided, the default title is used * * @public exported from `@promptbook/core` */ const DEFAULT_TASK_TITLE = `Task`; /** * When the title of the prompt task is not provided, the default title is used * * @public exported from `@promptbook/core` */ const DEFAULT_PROMPT_TASK_TITLE = `Prompt`; /** * When the pipeline is flat and no name of return parameter is provided, this name is used * * @public exported from `@promptbook/core` */ const DEFAULT_BOOK_OUTPUT_PARAMETER_NAME = 'result'; /** * Maximum file size limit * * @public exported from `@promptbook/core` */ const DEFAULT_MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB // <- TODO: [๐Ÿง ] Better system for generator warnings - not always "code" and "by `@promptbook/cli`" /** * The maximum number of iterations for a loops * * @private within the repository - too low-level in comparison with other `MAX_...` */ const LOOP_LIMIT = 1000; /** * Strings to represent various values in the context of parameter values * * @public exported from `@promptbook/utils` */ const VALUE_STRINGS = { empty: '(nothing; empty string)', null: '(no value; null)', undefined: '(unknown value; undefined)', nan: '(not a number; NaN)', infinity: '(infinity; โˆž)', negativeInfinity: '(negative infinity; -โˆž)', unserializable: '(unserializable value)', circular: '(circular JSON)', }; /** * Small number limit * * @public exported from `@promptbook/utils` */ const SMALL_NUMBER = 0.001; /** * Short time interval to prevent race conditions in milliseconds * * @private within the repository - too low-level in comparison with other `MAX_...` */ const IMMEDIATE_TIME = 10; /** * The maximum length of the (generated) filename * * @public exported from `@promptbook/core` */ const MAX_FILENAME_LENGTH = 30; /** * Strategy for caching the intermediate results for knowledge sources * * @public exported from `@promptbook/core` */ const DEFAULT_INTERMEDIATE_FILES_STRATEGY = 'HIDE_AND_KEEP'; // <- TODO: [๐Ÿ˜ก] Change to 'VISIBLE' /** * The maximum number of (LLM) tasks running in parallel * * @public exported from `@promptbook/core` */ const DEFAULT_MAX_PARALLEL_COUNT = 5; // <- TODO: [๐Ÿคนโ€โ™‚๏ธ] /** * The maximum number of attempts to execute LLM task before giving up * * @public exported from `@promptbook/core` */ const DEFAULT_MAX_EXECUTION_ATTEMPTS = 10; // <- TODO: [๐Ÿคนโ€โ™‚๏ธ] /** * @@@ * TODO: [๐Ÿ][main] !!3 Use * * @public exported from `@promptbook/core` */ const DEFAULT_MAX_KNOWLEDGE_SOURCES_SCRAPING_DEPTH = 3; /** * @@@ * TODO: [๐Ÿ][main] !!3 Use * * @public exported from `@promptbook/core` */ const DEFAULT_MAX_KNOWLEDGE_SOURCES_SCRAPING_TOTAL = 200; /** * Where to store your books * This is kind of a "src" for your books * * @public exported from `@promptbook/core` */ const DEFAULT_BOOKS_DIRNAME = './books'; // <- TODO: [๐Ÿ•] Make also `BOOKS_DIRNAME_ALTERNATIVES` // TODO: Just `.promptbook` in config, hardcode subfolders like `download-cache` or `execution-cache` /** * Where to store the temporary downloads * * Note: When the folder does not exist, it is created recursively * * @public exported from `@promptbook/core` */ const DEFAULT_DOWNLOAD_CACHE_DIRNAME = './.promptbook/download-cache'; /** * Where to store the cache of executions for promptbook CLI * * Note: When the folder does not exist, it is created recursively * * @public exported from `@promptbook/core` */ const DEFAULT_EXECUTION_CACHE_DIRNAME = './.promptbook/execution-cache'; /** * Where to store the scrape cache * * Note: When the folder does not exist, it is created recursively * * @public exported from `@promptbook/core` */ const DEFAULT_SCRAPE_CACHE_DIRNAME = './.promptbook/scrape-cache'; /** * Id of application for the CLI when using remote server * * @public exported from `@promptbook/core` */ const CLI_APP_ID = 'cli'; /** * Id of application for the playground * * @public exported from `@promptbook/core` */ const PLAYGROUND_APP_ID = 'playground'; /* TODO: [๐ŸŒƒ] /** * Id of application for the wizzard when using remote server * * @public exported from `@promptbook/core` * / ex-port const WIZZARD_APP_ID: string_app_id = 'wizzard'; */ /** * The name of the builded pipeline collection made by CLI `ptbk make` and for lookup in `createCollectionFromDirectory` * * @public exported from `@promptbook/core` */ const DEFAULT_PIPELINE_COLLECTION_BASE_FILENAME = `index`; /** * The thresholds for the relative time in the `moment` NPM package. * * @see https://momentjscom.readthedocs.io/en/latest/moment/07-customization/13-relative-time-threshold/ * @private within the repository - too low-level in comparison with other constants */ const MOMENT_ARG_THRESHOLDS = { ss: 3, // <- least number of seconds to be counted in seconds, minus 1. Must be set after setting the `s` unit or without setting the `s` unit. }; /** * Available remote servers for the Promptbook * * @public exported from `@promptbook/core` */ const REMOTE_SERVER_URLS = [ 'https://s1.ptbk.io/promptbook', 'https://api.pavolhejny.com/promptbook', ]; /** * Default remote server URL for the Promptbook * * @public exported from `@promptbook/core` */ const DEFAULT_REMOTE_SERVER_URL = REMOTE_SERVER_URLS[0]; // <- TODO: [๐Ÿงœโ€โ™‚๏ธ] /** * @@@ * * @public exported from `@promptbook/core` */ const DEFAULT_CSV_SETTINGS = Object.freeze({ delimiter: ',', quoteChar: '"', newline: '\n', skipEmptyLines: true, }); /** * @@@ * * @public exported from `@promptbook/core` */ exports.DEFAULT_IS_VERBOSE = false; /** * @@@ * * Note: This is experimental feature * * @public exported from `@promptbook/core` */ function SET_IS_VERBOSE(isVerbose) { exports.DEFAULT_IS_VERBOSE = isVerbose; } /** * @@@ * * @public exported from `@promptbook/core` */ const DEFAULT_IS_AUTO_INSTALLED = false; /** * Function name for generated function via `ptbk make` to get the pipeline collection * * @public exported from `@promptbook/core` */ const DEFAULT_GET_PIPELINE_COLLECTION_FUNCTION_NAME = `getPipelineCollection`; /** * @@@ * * @private within the repository */ const IS_PIPELINE_LOGIC_VALIDATED = just( /**/ // Note: In normal situations, we check the pipeline logic: true); /** * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name * TODO: [๐Ÿง ][๐Ÿงœโ€โ™‚๏ธ] Maybe join remoteServerUrl and path into single value */ /** * Make error report URL for the given error * * @private private within the repository */ function getErrorReportUrl(error) { const report = { title: `๐Ÿœ Error report from ${NAME}`, body: spaceTrim__default["default"]((block) => ` \`${error.name || 'Error'}\` has occurred in the [${NAME}], please look into it @${ADMIN_GITHUB_NAME}. \`\`\` ${block(error.message || '(no error message)')} \`\`\` ## More info: - **Promptbook engine version:** ${PROMPTBOOK_ENGINE_VERSION} - **Book language version:** ${BOOK_LANGUAGE_VERSION} - **Time:** ${new Date().toISOString()} <details> <summary>Stack trace:</summary> ## Stack trace: \`\`\`stacktrace ${block(error.stack || '(empty)')} \`\`\` </details> `), }; const reportUrl = new URL(`https://github.com/webgptorg/promptbook/issues/new`); reportUrl.searchParams.set('labels', 'bug'); reportUrl.searchParams.set('assignees', ADMIN_GITHUB_NAME); reportUrl.searchParams.set('title', report.title); reportUrl.searchParams.set('body', report.body); return reportUrl; } /** * This error type indicates that the error should not happen and its last check before crashing with some other error * * @public exported from `@promptbook/core` */ class UnexpectedError extends Error { constructor(message) { super(spaceTrim.spaceTrim((block) => ` ${block(message)} Note: This error should not happen. It's probbably a bug in the pipeline collection Please report issue: ${block(getErrorReportUrl(new Error(message)).href)} Or contact us on ${ADMIN_EMAIL} `)); this.name = 'UnexpectedError'; Object.setPrototypeOf(this, UnexpectedError.prototype); } } /** * This error type indicates that somewhere in the code non-Error object was thrown and it was wrapped into the `WrappedError` * * @public exported from `@promptbook/core` */ class WrappedError extends Error { constructor(whatWasThrown) { const tag = `[๐Ÿคฎ]`; console.error(tag, whatWasThrown); super(spaceTrim.spaceTrim(` Non-Error object was thrown Note: Look for ${tag} in the console for more details Please report issue on ${ADMIN_EMAIL} `)); this.name = 'WrappedError'; Object.setPrototypeOf(this, WrappedError.prototype); } } /** * Helper used in catch blocks to assert that the error is an instance of `Error` * * @param whatWasThrown Any object that was thrown * @returns Nothing if the error is an instance of `Error` * @throws `WrappedError` or `UnexpectedError` if the error is not standard * * @private within the repository */ function assertsError(whatWasThrown) { // Case 1: Handle error which was rethrown as `WrappedError` if (whatWasThrown instanceof WrappedError) { const wrappedError = whatWasThrown; throw wrappedError; } // Case 2: Handle unexpected errors if (whatWasThrown instanceof UnexpectedError) { const unexpectedError = whatWasThrown; throw unexpectedError; } // Case 3: Handle standard errors - keep them up to consumer if (whatWasThrown instanceof Error) { return; } // Case 4: Handle non-standard errors - wrap them into `WrappedError` and throw throw new WrappedError(whatWasThrown); } /** * Function isValidJsonString will tell you if the string is valid JSON or not * * @public exported from `@promptbook/utils` */ function isValidJsonString(value /* <- [๐Ÿ‘จโ€โš–๏ธ] */) { try { JSON.parse(value); return true; } catch (error) { assertsError(error); if (error.message.includes('Unexpected token')) { return false; } return false; } } /** * Function `validatePipelineString` will validate the if the string is a valid pipeline string * It does not check if the string is fully logically correct, but if it is a string that can be a pipeline string or the string looks completely different. * * @param {string} pipelineString the candidate for a pipeline string * @returns {PipelineString} the same string as input, but validated as valid * @throws {ParseError} if the string is not a valid pipeline string * @public exported from `@promptbook/core` */ function validatePipelineString(pipelineString) { if (isValidJsonString(pipelineString)) { throw new ParseError('Expected a book, but got a JSON string'); } else if (isValidUrl(pipelineString)) { throw new ParseError(`Expected a book, but got just the URL "${pipelineString}"`); } else if (isValidFilePath(pipelineString)) { throw new ParseError(`Expected a book, but got just the file path "${pipelineString}"`); } else if (isValidEmail(pipelineString)) { throw new ParseError(`Expected a book, but got just the email "${pipelineString}"`); } // <- TODO: Implement the validation + add tests when the pipeline logic considered as invalid return pipelineString; } /** * TODO: [๐Ÿง ][๐Ÿˆด] Where is the best location for this file */ /** * Prettify the html code * * @param content raw html code * @returns formatted html code * @private withing the package because of HUGE size of prettier dependency */ function prettifyMarkdown(content) { try { return prettier.format(content, { parser: 'markdown', plugins: [parserHtml__default["default"]], // TODO: DRY - make some import or auto-copy of .prettierrc endOfLine: 'lf', tabWidth: 4, singleQuote: true, trailingComma: 'all', arrowParens: 'always', printWidth: 120, htmlWhitespaceSensitivity: 'ignore', jsxBracketSameLine: false, bracketSpacing: true, }); } catch (error) { // TODO: [๐ŸŸฅ] Detect browser / node and make it colorfull console.error('There was an error with prettifying the markdown, using the original as the fallback', { error, html: content, }); return content; } } /** * Makes first letter of a string uppercase * * @public exported from `@promptbook/utils` */ function capitalize(word) { return word.substring(0, 1).toUpperCase() + word.substring(1); } /** * Converts promptbook in JSON format to string format * * @deprecated TODO: [๐Ÿฅ][๐Ÿง ] Backup original files in `PipelineJson` same as in Promptbook.studio * @param pipelineJson Promptbook in JSON format (.bookc) * @returns Promptbook in string format (.book.md) * @public exported from `@promptbook/core` */ function pipelineJsonToString(pipelineJson) { const { title, pipelineUrl, bookVersion, description, parameters, tasks } = pipelineJson; let pipelineString = `# ${title}`; if (description) { pipelineString += '\n\n'; pipelineString += description; } const commands = []; if (pipelineUrl) { commands.push(`PIPELINE URL ${pipelineUrl}`); } if (bookVersion !== `undefined`) { commands.push(`BOOK VERSION ${bookVersion}`); } // TODO: [main] !!5 This increases size of the bundle and is probbably not necessary pipelineString = prettifyMarkdown(pipelineString); for (const parameter of parameters.filter(({ isInput }) => isInput)) { commands.push(`INPUT PARAMETER ${taskParameterJsonToString(parameter)}`); } for (const parameter of parameters.filter(({ isOutput }) => isOutput)) { commands.push(`OUTPUT PARAMETER ${taskParameterJsonToString(parameter)}`); } pipelineString += '\n\n'; pipelineString += commands.map((command) => `- ${command}`).join('\n'); for (const task of tasks) { const { /* Note: Not using:> name, */ title, description, /* Note: dependentParameterNames, */ jokerParameterNames: jokers, taskType, content, postprocessingFunctionNames: postprocessing, expectations, format, resultingParameterName, } = task; pipelineString += '\n\n'; pipelineString += `## ${title}`; if (description) { pipelineString += '\n\n'; pipelineString += description; } const commands = []; let contentLanguage = 'text'; if (taskType === 'PROMPT_TASK') { const { modelRequirements } = task; const { modelName, modelVariant } = modelRequirements || {}; // Note: Do nothing, it is default // commands.push(`PROMPT`); if (modelVariant) { commands.push(`MODEL VARIANT ${capitalize(modelVariant)}`); } if (modelName) { commands.push(`MODEL NAME \`${modelName}\``); } } else if (taskType === 'SIMPLE_TASK') { commands.push(`SIMPLE TEMPLATE`); // Note: Nothing special here } else if (taskType === 'SCRIPT_TASK') { commands.push(`SCRIPT`); if (task.contentLanguage) { contentLanguage = task.contentLanguage; } else { contentLanguage = ''; } } else if (taskType === 'DIALOG_TASK') { commands.push(`DIALOG`); // Note: Nothing special here } // <- }else if([๐Ÿ…ฑ] if (jokers) { for (const joker of jokers) { commands.push(`JOKER {${joker}}`); } } /* not else */ if (postprocessing) { for (const postprocessingFunctionName of postprocessing) { commands.push(`POSTPROCESSING \`${postprocessingFunctionName}\``); } } /* not else */ if (expectations) { for (const [unit, { min, max }] of Object.entries(expectations)) { if (min === max) { commands.push(`EXPECT EXACTLY ${min} ${capitalize(unit + (min > 1 ? 's' : ''))}`); } else { if (min !== undefined) { commands.push(`EXPECT MIN ${min} ${capitalize(unit + (min > 1 ? 's' : ''))}`); } /* not else */ if (max !== undefined) { commands.push(`EXPECT MAX ${max} ${capitalize(unit + (max > 1 ? 's' : ''))}`); } } } } /* not else */ if (format) { if (format === 'JSON') { // TODO: @deprecated remove commands.push(`FORMAT JSON`); } } /* not else */ pipelineString += '\n\n'; pipelineString += commands.map((command) => `- ${command}`).join('\n'); pipelineString += '\n\n'; pipelineString += '```' + contentLanguage; pipelineString += '\n'; pipelineString += spaceTrim__default["default"](content); // <- TODO: [main] !!3 Escape // <- TODO: [๐Ÿง ] Some clear strategy how to spaceTrim the blocks pipelineString += '\n'; pipelineString += '```'; pipelineString += '\n\n'; pipelineString += `\`-> {${resultingParameterName}}\``; // <- TODO: [main] !!3 If the parameter here has description, add it and use taskParameterJsonToString } return validatePipelineString(pipelineString); } /** * @private internal utility of `pipelineJsonToString` */ function taskParameterJsonToString(taskParameterJson) { const { name, description } = taskParameterJson; let parameterString = `{${name}}`; if (description) { parameterString = `${parameterString} ${description}`; } return parameterString; } /** * TODO: [๐Ÿ›‹] Implement new features and commands into `pipelineJsonToString` + `taskParameterJsonToString` , use `stringifyCommand` * TODO: [๐Ÿง ] Is there a way to auto-detect missing features in pipelineJsonToString * TODO: [๐Ÿ›] Maybe make some markdown builder * TODO: [๐Ÿ›] Escape all * TODO: [๐Ÿง ] Should be in generated .book.md file GENERATOR_WARNING */ /** * Orders JSON object by keys * * @returns The same type of object as the input re-ordered * @public exported from `@promptbook/utils` */ function orderJson(options) { const { value, order } = options; const orderedValue = { ...(order === undefined ? {} : Object.fromEntries(order.map((key) => [key, undefined]))), ...value, }; return orderedValue; } /** * Freezes the given object and all its nested objects recursively * * Note: `$` is used to indicate that this function is not a pure function - it mutates given object * Note: This function mutates the object and returns the original (but mutated-deep-freezed) object * * @returns The same object as the input, but deeply frozen * @public exported from `@promptbook/utils` */ function $deepFreeze(objectValue) { if (Array.isArray(objectValue)) { return Object.freeze(objectValue.map((item) => $deepFreeze(item))); } const propertyNames = Object.getOwnPropertyNames(objectValue); for (const propertyName of propertyNames) { const value = objectValue[propertyName]; if (value && typeof value === 'object') { $deepFreeze(value); } } Object.freeze(objectValue); return objectValue; } /** * TODO: [๐Ÿง ] Is there a way how to meaningfully test this utility */ /** * Checks if the value is [๐Ÿš‰] serializable as JSON * If not, throws an UnexpectedError with a rich error message and tracking * * - Almost all primitives are serializable BUT: * - `undefined` is not serializable * - `NaN` is not serializable * - Objects and arrays are serializable if all their properties are serializable * - Functions are not serializable * - Circular references are not serializable * - `Date` objects are not serializable * - `Map` and `Set` objects are not serializable * - `RegExp` objects are not serializable * - `Error` objects are not serializable * - `Symbol` objects are not serializable * - And much more... * * @throws UnexpectedError if the value is not serializable as JSON * @public exported from `@promptbook/utils` */ function checkSerializableAsJson(options) { const { value, name, message } = options; if (value === undefined) { throw new UnexpectedError(`${name} is undefined`); } else if (value === null) { return; } else if (typeof value === 'boolean') { return; } else if (typeof value === 'number' && !isNaN(value)) { return; } else if (typeof value === 'string') { return; } else if (typeof value === 'symbol') { throw new UnexpectedError(`${name} is symbol`); } else if (typeof value === 'function') { throw new UnexpectedError(`${name} is function`); } else if (typeof value === 'object' && Array.isArray(value)) { for (let i = 0; i < value.length; i++) { checkSerializableAsJson({ name: `${name}[${i}]`, value: value[i], message }); } } else if (typeof value === 'object') { if (value instanceof Date) { throw new UnexpectedError(spaceTrim__default["default"]((block) => ` \`${name}\` is Date Use \`string_date_iso8601\` instead Additional message for \`${name}\`: ${block(message || '(nothing)')} `)); } else if (value instanceof Map) { throw new UnexpectedError(`${name} is Map`); } else if (value instanceof Set) { throw new UnexpectedError(`${name} is Set`); } else if (value instanceof RegExp) { throw new UnexpectedError(`${name} is RegExp`); } else if (value instanceof Error) { throw new UnexpectedError(spaceTrim__default["default"]((block) => ` \`${name}\` is unserialized Error Use function \`serializeError\` Additional message for \`${name}\`: ${block(message || '(nothing)')} `)); } else { for (const [subName, subValue] of Object.entries(value)) { if (subValue === undefined) { // Note: undefined in object is serializable - it is just omited continue; } checkSerializableAsJson({ name: `${name}.${subName}`, value: subValue, message }); } try { JSON.stringify(value); // <- TODO: [0] } catch (error) { assertsError(error); throw new UnexpectedError(spaceTrim__default["default"]((block) => ` \`${name}\` is not serializable ${block(error.stack || error.message)} Additional message for \`${name}\`: ${block(message || '(nothing)')} `)); } /* TODO: [0] Is there some more elegant way to check circular references? const seen = new Set(); const stack = [{ value }]; while (stack.length > 0) { const { value } = stack.pop()!; if (typeof value === 'object' && value !== null) { if (seen.has(value)) { throw new UnexpectedError(`${name} has circular reference`); } seen.add(value); if (Array.isArray(value)) { stack.push(...value.map((value) => ({ value }))); } else { stack.push(...Object.values(value).map((value) => ({ value }))); } } } */ return; } } else { throw new UnexpectedError(spaceTrim__default["default"]((block) => ` \`${name}\` is unknown type Additional message for \`${name}\`: ${block(message || '(nothing)')} `)); } } /** * TODO: Can be return type more type-safe? like `asserts options.value is JsonValue` * TODO: [๐Ÿง ][main] !!3 In-memory cache of same values to prevent multiple checks * Note: [๐Ÿ ] This is how `checkSerializableAsJson` + `isSerializableAsJson` together can just retun true/false or rich error message */ /** * @@@ * * @public exported from `@promptbook/utils` */ function deepClone(objectValue) { return JSON.parse(JSON.stringify(objectValue)); /* TODO: [๐Ÿง ] Is there a better implementation? > const propertyNames = Object.getOwnPropertyNames(objectValue); > for (const propertyName of propertyNames) { > const value = (objectValue as really_any)[propertyName]; > if (value && typeof value === 'object') { > deepClone(value); > } > } > return Object.assign({}, objectValue); */ } /** * TODO: [๐Ÿง ] Is there a way how to meaningfully test this utility */ /** * Utility to export a JSON object from a function * * 1) Checks if the value is serializable as JSON * 2) Makes a deep clone of the object * 2) Orders the object properties * 2) Deeply freezes the cloned object * * Note: This function does not mutates the given object * * @returns The same type of object as the input but read-only and re-ordered * @public exported from `@promptbook/utils` */ function exportJson(options) { const { name, value, order, message } = options; checkSerializableAsJson({ name, value, message }); const orderedValue = // TODO: Fix error "Type instantiation is excessively deep and possibly infinite." // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore order === undefined ? deepClone(value) : orderJson({ value: value, // <- Note: checkSerializableAsJson asserts that the value is serializable as JSON order: order, }); $deepFreeze(orderedValue); return orderedValue; } /** * TODO: [๐Ÿง ] Is there a way how to meaningfully test this utility */ /** * Order of keys in the pipeline JSON * * @public exported from `@promptbook/core` */ const ORDER_OF_PIPELINE_JSON = [ // Note: [๐Ÿ™] In this order will be pipeline serialized 'title', 'pipelineUrl', 'bookVersion', 'description', 'formfactorName', 'parameters', 'tasks', 'personas', 'preparations', 'knowledgeSources', 'knowledgePieces', 'sources', // <- TODO: [๐Ÿง ] Where should the `sources` be ]; /** * Nonce which is used for replacing things in strings * * @private within the repository */ const REPLACING_NONCE = 'ptbkauk42kV2dzao34faw7FudQUHYPtW'; /** * @@@ * * @private within the repository */ const RESERVED_PARAMETER_MISSING_VALUE = 'MISSING-' + REPLACING_NONCE; /** * @@@ * * @private within the repository */ const RESERVED_PARAMETER_RESTRICTED = 'RESTRICTED-' + REPLACING_NONCE; /** * The names of the parameters that are reserved for special purposes * * @public exported from `@promptbook/core` */ const RESERVED_PARAMETER_NAMES = exportJson({ name: 'RESERVED_PARAMETER_NAMES', message: `The names of the parameters that are reserved for special purposes`, value: [ 'content', 'context', 'knowledge', 'examples', 'modelName', 'currentDate', // <- TODO: list here all command names // <- TODO: Add more like 'date', 'modelName',... // <- TODO: Add [emoji] + instructions ACRY when adding new reserved parameter ], }); /** * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name */ /** * This error indicates that the promptbook object has valid syntax (=can be parsed) but contains logical errors (like circular dependencies) * * @public exported from `@promptbook/core` */ class PipelineLogicError extends Error { constructor(message) { super(message); this.name = 'PipelineLogicError'; Object.setPrototypeOf(this, PipelineLogicError.prototype); } } /** * Tests if given string is valid semantic version * * Note: There are two simmilar functions: * - `isValidSemanticVersion` which tests any semantic version * - `isValidPromptbookVersion` *(this one)* which tests just Promptbook versions * * @public exported from `@promptbook/utils` */ function isValidSemanticVersion(version) { if (typeof version !== 'string') { return false; } if (version.startsWith('0.0.0')) { return false; } return /^\d+\.\d+\.\d+(-\d+)?$/i.test(version); } /** * Tests if given string is valid promptbook version * It looks into list of known promptbook versions. * * @see https://www.npmjs.com/package/promptbook?activeTab=versions * Note: When you are using for example promptbook 2.0.0 and there already is promptbook 3.0.0 it don`t know about it. * Note: There are two simmilar functions: * - `isValidSemanticVersion` which tests any semantic version * - `isValidPromptbookVersion` *(this one)* which tests just Promptbook versions * * @public exported from `@promptbook/utils` */ function isValidPromptbookVersion(version) { if (!isValidSemanticVersion(version)) { return false; } if ( /* version === '1.0.0' || */version === '2.0.0' || version === '3.0.0') { return false; } // <- TODO: [main] !!3 Check isValidPromptbookVersion against PROMPTBOOK_ENGINE_VERSIONS return true; } /** * Tests if given string is valid pipeline URL URL. * * Note: There are two simmilar functions: * - `isValidUrl` which tests any URL * - `isValidPipelineUrl` *(this one)* which tests just pipeline URL * * @public exported from `@promptbook/utils` */ function isValidPipelineUrl(url) { if (!isValidUrl(url)) { return false; } if (!url.startsWith('https://') && !url.startsWith('http://') /* <- Note: [๐Ÿ‘ฃ] */) { return false; } if (url.includes('#')) { // TODO: [๐Ÿ ] return false; } /* Note: [๐Ÿ‘ฃ][๐Ÿง ] Is it secure to allow pipeline URLs on private and unsecured networks? if (isUrlOnPrivateNetwork(url)) { return false; } */ return true; } /** * TODO: [๐Ÿ ] Maybe more info why the URL is invalid */ /** * Validates PipelineJson if it is logically valid * * It checks: * - if it has correct parameters dependency * * It does NOT check: * - if it is valid json * - if it is meaningful * * @param pipeline valid or invalid PipelineJson * @returns the same pipeline if it is logically valid * @throws {PipelineLogicError} on logical error in the pipeline * @public exported from `@promptbook/core` */ function validatePipeline(pipeline) { if (IS_PIPELINE_LOGIC_VALIDATED) { validatePipeline_InnerFunction(pipeline); } else { try { validatePipeline_InnerFunction(pipeline); } catch (error) { if (!(error instanceof PipelineLogicError)) { throw error; } console.error(spaceTrim.spaceTrim((block) => ` Pipeline is not valid but logic errors are temporarily disabled via \`IS_PIPELINE_LOGIC_VALIDATED\` ${block(error.message)} `)); } } return pipeline; } /** * @private internal function for `validatePipeline` */ function validatePipeline_InnerFunction(pipeline) { // TODO: [๐Ÿง ] Maybe test if promptbook is a promise and make specific error case for that const pipelineIdentification = (() => { // Note: This is a ๐Ÿ˜ implementation of [๐Ÿšž] const _ = []; if (pipeline.sourceFile !== undefined) { _.push(`File: ${pipeline.sourceFile}`); } if (pipeline.pipelineUrl !== undefined) { _.push(`Url: ${pipeline.pipelineUrl}`); } return _.join('\n'); })(); if (pipeline.pipelineUrl !== undefined && !isValidPipelineUrl(pipeline.pipelineUrl)) { // <- Note: [๐Ÿšฒ] throw new PipelineLogicError(spaceTrim.spaceTrim((block) => ` Invalid promptbook URL "${pipeline.pipelineUrl}" ${block(pipelineIdentification)} `)); } if (pipeline.bookVersion !== undefined && !isValidPromptbookVersion(pipeline.bookVersion)) { // <- Note: [๐Ÿšฒ] throw new PipelineLogicError(spaceTrim.spaceTrim((block) => ` Invalid Promptbook Version "${pipeline.bookVersion}" ${block(pipelineIdentification)} `)); } // TODO: [๐Ÿง ] Maybe do here some propper JSON-schema / ZOD checking if (!Array.isArray(pipeline.parameters)) { // TODO: [๐Ÿง ] what is the correct error tp throw - maybe PromptbookSchemaError throw new ParseError(spaceTrim.spaceTrim((block) => ` Pipeline is valid JSON but with wrong structure \`PipelineJson.parameters\` expected to be an array, but got ${typeof pipeline.parameters} ${block(pipelineIdentification)} `)); } // TODO: [๐Ÿง ] Maybe do here some propper JSON-schema / ZOD checking if (!Array.isArray(pipeline.tasks)) { // TODO: [๐Ÿง ] what is the correct error tp throw - maybe PromptbookSchemaError throw new ParseError(spaceTrim.spaceTrim((block) => ` Pipeline is valid JSON but with wrong structure \`PipelineJson.tasks\` expected to be an array, but got ${typeof pipeline.tasks} ${block(pipelineIdentification)} `)); } /* TODO: [๐Ÿง ][๐Ÿ…พ] Should be empty pipeline valid or not // Note: Check that pipeline has some tasks if (pipeline.tasks.length === 0) { throw new PipelineLogicError( spaceTrim( (block) => ` Pipeline must have at least one task ${block(pipelineIdentification)} `, ), ); } */ // Note: Check each parameter individually for (const parameter of pipeline.parameters) { if (parameter.isInput && parameter.isOutput) { throw new PipelineLogicError(spaceTrim.spaceTrim((block) => ` Parameter \`{${parameter.name}}\` can not be both input and output ${block(pipelineIdentification)} `)); } // Note: Testing that parameter is either intermediate or output BUT not created and unused if (!parameter.isInput && !parameter.isOutput && !pipeline.tasks.some((task) => task.dependentParameterNames.includes(parameter.name))) { throw new PipelineLogicError(spaceTrim.spaceTrim((block) => ` Parameter \`{${parameter.name}}\` is created but not used You can declare {${parameter.name}} as output parameter by adding in the header: - OUTPUT PARAMETER \`{${parameter.name}}\` ${parameter.description || ''} ${block(pipelineIdentification)} `)); } // Note: Testing that parameter is either input or result of some task if (!parameter.isInput && !pipeline.tasks.some((task) => task.resultingParameterName === parameter.name)) { throw new PipelineLogicError(spaceTrim.spaceTrim((block) => ` Parameter \`{${parameter.name}}\` is declared but not defined You can do one of these: 1) Remove declaration of \`{${parameter.name}}\` 2) Add task that results in \`-> {${parameter.name}}\` ${block(pipelineIdentification)} `)); } } // Note: All input parameters are defined - so that they can be used as result of some task const definedParameters = new Set(pipeline.parameters.filter(({ isInput }) => isInput).map(({ name }) => name)); // Note: Checking each task individually for (const task of pipeline.tasks) { if (definedParameters.has(task.resultingParameterName)) { throw new PipelineLogicError(spaceTrim.spaceTrim((block) => ` Parameter \`{${task.resultingParameterName}}\` is defined multiple times ${block(pipelineIdentification)} `)); } if (RESERVED_PARAMETER_NAMES.includes(task.resultingParameterName)) { throw new PipelineLogicError(spaceTrim.spaceTrim((block) => ` Parameter name {${task.resultingParameterName}} is reserved, please use different name ${block