UNPKG

@promptbook/remote-server

Version:

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

1,401 lines (1,318 loc) โ€ข 321 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('colors'), require('express'), require('http'), require('socket.io'), require('spacetrim'), require('swagger-jsdoc'), require('swagger-ui-express'), require('waitasecond'), require('crypto'), require('child_process'), require('fs/promises'), require('path'), require('rxjs'), require('prettier'), require('prettier/parser-html'), require('crypto-js/enc-hex'), require('crypto-js/sha256'), require('crypto-js'), require('mime-types'), require('papaparse')) : typeof define === 'function' && define.amd ? define(['exports', 'colors', 'express', 'http', 'socket.io', 'spacetrim', 'swagger-jsdoc', 'swagger-ui-express', 'waitasecond', 'crypto', 'child_process', 'fs/promises', 'path', 'rxjs', 'prettier', 'prettier/parser-html', 'crypto-js/enc-hex', 'crypto-js/sha256', 'crypto-js', 'mime-types', 'papaparse'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["promptbook-remote-server"] = {}, global.colors, global.express, global.http, global.socket_io, global.spaceTrim, global.swaggerJsdoc, global.swaggerUi, global.waitasecond, global.crypto, global.child_process, global.promises, global.path, global.rxjs, global.prettier, global.parserHtml, global.hexEncoder, global.sha256, global.cryptoJs, global.mimeTypes, global.papaparse)); })(this, (function (exports, colors, express, http, socket_io, spaceTrim, swaggerJsdoc, swaggerUi, waitasecond, crypto, child_process, promises, path, rxjs, prettier, parserHtml, hexEncoder, sha256, cryptoJs, mimeTypes, papaparse) { 'use strict'; function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var colors__default = /*#__PURE__*/_interopDefaultLegacy(colors); var express__default = /*#__PURE__*/_interopDefaultLegacy(express); var http__default = /*#__PURE__*/_interopDefaultLegacy(http); var spaceTrim__default = /*#__PURE__*/_interopDefaultLegacy(spaceTrim); var swaggerJsdoc__default = /*#__PURE__*/_interopDefaultLegacy(swaggerJsdoc); var swaggerUi__default = /*#__PURE__*/_interopDefaultLegacy(swaggerUi); var parserHtml__default = /*#__PURE__*/_interopDefaultLegacy(parserHtml); var hexEncoder__default = /*#__PURE__*/_interopDefaultLegacy(hexEncoder); var sha256__default = /*#__PURE__*/_interopDefaultLegacy(sha256); // โš ๏ธ 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 */ /** * Error thrown when a fetch request fails * * @public exported from `@promptbook/core` */ class PromptbookFetchError extends Error { constructor(message) { super(message); this.name = 'PromptbookFetchError'; Object.setPrototypeOf(this, PromptbookFetchError.prototype); } } /** * 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; } /** * 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`; /** * 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: [๐Ÿ•] 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 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'; // <- TODO: [๐Ÿงœโ€โ™‚๏ธ] /** * @@@ * * @public exported from `@promptbook/core` */ const DEFAULT_CSV_SETTINGS = Object.freeze({ delimiter: ',', quoteChar: '"', newline: '\n', skipEmptyLines: true, }); /** * @@@ * * @public exported from `@promptbook/core` */ let DEFAULT_IS_VERBOSE = false; /** * @@@ * * @public exported from `@promptbook/core` */ const DEFAULT_IS_AUTO_INSTALLED = false; /** * @@@ * * @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); } /** * The built-in `fetch' function with a lightweight error handling wrapper as default fetch function used in Promptbook scrapers * * @public exported from `@promptbook/core` */ const promptbookFetch = async (urlOrRequest, init) => { try { return await fetch(urlOrRequest, init); } catch (error) { assertsError(error); let url; if (typeof urlOrRequest === 'string') { url = urlOrRequest; } else if (urlOrRequest instanceof Request) { url = urlOrRequest.url; } throw new PromptbookFetchError(spaceTrim__default["default"]((block) => ` Can not fetch "${url}" Fetch error: ${block(error.message)} `)); } }; /** * TODO: [๐Ÿง ] Maybe rename because it is not used only for scrapers but also in `$getCompiledBook` */ /** * AuthenticationError is thrown from login function which is dependency of remote server * * @public exported from `@promptbook/core` */ class AuthenticationError extends Error { constructor(message) { super(message); this.name = 'AuthenticationError'; Object.setPrototypeOf(this, AuthenticationError.prototype); } } /** * Generates random token * * Note: This function is cryptographically secure (it uses crypto.randomBytes internally) * * @private internal helper function * @returns secure random token */ function $randomToken(randomness) { return crypto.randomBytes(randomness).toString('hex'); } /** * TODO: Maybe use nanoid instead https://github.com/ai/nanoid */ /** * This error indicates errors during the execution of the pipeline * * @public exported from `@promptbook/core` */ class PipelineExecutionError extends Error { constructor(message) { // Added id parameter super(message); this.name = 'PipelineExecutionError'; // TODO: [๐Ÿ™] DRY - Maybe $randomId this.id = `error-${$randomToken(8 /* <- TODO: To global config + Use Base58 to avoid simmilar char conflicts */)}`; Object.setPrototypeOf(this, PipelineExecutionError.prototype); } } /** * TODO: [๐Ÿง ][๐ŸŒ‚] Add id to all errors */ /** * This error indicates problems parsing the format value * * For example, when the format value is not a valid JSON or CSV * This is not thrown directly but in extended classes * * @public exported from `@promptbook/core` */ class AbstractFormatError extends Error { // Note: To allow instanceof do not put here error `name` // public readonly name = 'AbstractFormatError'; constructor(message) { super(message); Object.setPrototypeOf(this, AbstractFormatError.prototype); } } /** * This error indicates problem with parsing of CSV * * @public exported from `@promptbook/core` */ class CsvFormatError extends AbstractFormatError { constructor(message) { super(message); this.name = 'CsvFormatError'; Object.setPrototypeOf(this, CsvFormatError.prototype); } } /** * This error indicates that the pipeline collection cannot be propperly loaded * * @public exported from `@promptbook/core` */ class CollectionError extends Error { constructor(message) { super(message); this.name = 'CollectionError'; Object.setPrototypeOf(this, CollectionError.prototype); } } /** * This error type indicates that you try to use a feature that is not available in the current environment * * @public exported from `@promptbook/core` */ class EnvironmentMismatchError extends Error { constructor(message) { super(message); this.name = 'EnvironmentMismatchError'; Object.setPrototypeOf(this, EnvironmentMismatchError.prototype); } } /** * This error occurs when some expectation is not met in the execution of the pipeline * * @public exported from `@promptbook/core` * Note: Do not throw this error, its reserved for `checkExpectations` and `createPipelineExecutor` and public ONLY to be serializable through remote server * Note: Always thrown in `checkExpectations` and catched in `createPipelineExecutor` and rethrown as `PipelineExecutionError` * Note: This is a kindof subtype of PipelineExecutionError */ class ExpectError extends Error { constructor(message) { super(message); this.name = 'ExpectError'; Object.setPrototypeOf(this, ExpectError.prototype); } } /** * This error indicates that the promptbook can not retrieve knowledge from external sources * * @public exported from `@promptbook/core` */ class KnowledgeScrapeError extends Error { constructor(message) { super(message); this.name = 'KnowledgeScrapeError'; Object.setPrototypeOf(this, KnowledgeScrapeError.prototype); } } /** * This error type indicates that some limit was reached * * @public exported from `@promptbook/core` */ class LimitReachedError extends Error { constructor(message) { super(message); this.name = 'LimitReachedError'; Object.setPrototypeOf(this, LimitReachedError.prototype); } } /** * This error type indicates that some tools are missing for pipeline execution or preparation * * @public exported from `@promptbook/core` */ class MissingToolsError extends Error { constructor(message) { super(spaceTrim.spaceTrim((block) => ` ${block(message)} Note: You have probbably forgot to provide some tools for pipeline execution or preparation `)); this.name = 'MissingToolsError'; Object.setPrototypeOf(this, MissingToolsError.prototype); } } /** * This error indicates that promptbook not found in the collection * * @public exported from `@promptbook/core` */ class NotFoundError extends Error { constructor(message) { super(message); this.name = 'NotFoundError'; Object.setPrototypeOf(this, NotFoundError.prototype); } } /** * This error type indicates that some part of the code is not implemented yet * * @public exported from `@promptbook/core` */ class NotYetImplementedError extends Error { constructor(message) { super(spaceTrim.spaceTrim((block) => ` ${block(message)} Note: This feature is not implemented yet but it will be soon. If you want speed up the implementation or just read more, look here: https://github.com/webgptorg/promptbook Or contact us on pavol@ptbk.io `)); this.name = 'NotYetImplementedError'; Object.setPrototypeOf(this, NotYetImplementedError.prototype); } } /** * 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` */ /** * 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); } } /** * This error indicates errors in referencing promptbooks between each other * * @public exported from `@promptbook/core` */ class PipelineUrlError extends Error { constructor(message) { super(message); this.name = 'PipelineUrlError'; Object.setPrototypeOf(this, PipelineUrlError.prototype); } } /** * Index of all custom errors * * @public exported from `@promptbook/core` */ const PROMPTBOOK_ERRORS = { AbstractFormatError, CsvFormatError, CollectionError, EnvironmentMismatchError, ExpectError, KnowledgeScrapeError, LimitReachedError, MissingToolsError, NotFoundError, NotYetImplementedError, ParseError, PipelineExecutionError, PipelineLogicError, PipelineUrlError, AuthenticationError, PromptbookFetchError, UnexpectedError, WrappedError, // TODO: [๐Ÿช‘]> VersionMismatchError, }; /** * Index of all javascript errors * * @private for internal usage */ const COMMON_JAVASCRIPT_ERRORS = { Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError, AggregateError, /* Note: Not widely supported > InternalError, > ModuleError, > HeapError, > WebAssemblyCompileError, > WebAssemblyRuntimeError, */ }; /** * Index of all errors * * @private for internal usage */ const ALL_ERRORS = { ...PROMPTBOOK_ERRORS, ...COMMON_JAVASCRIPT_ERRORS, }; /** * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name */ /** * Serializes an error into a [๐Ÿš‰] JSON-serializable object * * @public exported from `@promptbook/utils` */ function serializeError(error) { const { name, message, stack } = error; const { id } = error; if (!Object.keys(ALL_ERRORS).includes(name)) { console.error(spaceTrim__default["default"]((block) => ` Cannot serialize error with name "${name}" Authors of Promptbook probably forgot to add this error into the list of errors: https://github.com/webgptorg/promptbook/blob/main/src/errors/0-index.ts ${block(stack || message)} `)); } return { name: name, message, stack, id, // Include id in the serialized object }; } /** * Detects if the code is running in a Node.js environment * * Note: `$` is used to indicate that this function is not a pure function - it looks at the global object to determine the environment * * @public exported from `@promptbook/utils` */ const $isRunningInNode = new Function(` try { return this === global; } catch (e) { return false; } `); /** * TODO: [๐ŸŽบ] */ /** * Normalize options for `execCommand` and `execCommands` * * Note: `$` is used to indicate that this function behaves differently according to `process.platform` * * @private internal utility of `execCommand` and `execCommands` */ function $execCommandNormalizeOptions(options) { var _a, _b, _c, _d; let command; let cwd; let crashOnError; let args = []; let timeout; let isVerbose; if (typeof options === 'string') { // TODO: [1] DRY default values command = options; cwd = process.cwd(); crashOnError = true; timeout = Infinity; // <- TODO: [โณ] isVerbose = DEFAULT_IS_VERBOSE; } else { /* TODO: if ((options as any).commands !== undefined) { commands = (options as any).commands; } else { commands = [(options as any).command]; } */ // TODO: [1] DRY default values command = options.command; cwd = (_a = options.cwd) !== null && _a !== void 0 ? _a : process.cwd(); crashOnError = (_b = options.crashOnError) !== null && _b !== void 0 ? _b : true; timeout = (_c = options.timeout) !== null && _c !== void 0 ? _c : Infinity; isVerbose = (_d = options.isVerbose) !== null && _d !== void 0 ? _d : DEFAULT_IS_VERBOSE; } // TODO: /(-[a-zA-Z0-9-]+\s+[^\s]*)|[^\s]*/g const _ = Array.from(command.matchAll(/(".*")|([^\s]*)/g)) .map(([match]) => match) .filter((arg) => arg !== ''); if (_.length > 1) { [command, ...args] = _; } if (options.args) { args = [...args, ...options.args]; } let humanReadableCommand = !['npx', 'npm'].includes(command) ? command : args[0]; if (['ts-node'].includes(humanReadableCommand)) { humanReadableCommand += ` ${args[1]}`; } if (/^win/.test(process.platform) && ['npm', 'npx'].includes(command)) { command = `${command}.cmd`; } return { command, humanReadableCommand, args, cwd, crashOnError, timeout, isVerbose }; } // TODO: This should show type error> execCommandNormalizeOptions({ command: '', commands: [''] }); /** * Run one command in a shell * * * Note: There are 2 similar functions in the codebase: * - `$execCommand` which runs a single command * - `$execCommands` which runs multiple commands * Note: `$` is used to indicate that this function is not a pure function - it runs a command in a shell * * @public exported from `@promptbook/node` */ function $execCommand(options) { if (!$isRunningInNode()) { throw new EnvironmentMismatchError('Function `$execCommand` can run only in Node environment.js'); } return new Promise((resolve, reject) => { // eslint-disable-next-line prefer-const const { command, humanReadableCommand, args, cwd, crashOnError, timeout, isVerbose = DEFAULT_IS_VERBOSE, } = $execCommandNormalizeOptions(options); if (timeout !== Infinity) { // TODO: In waitasecond forTime(Infinity) should be equivalent to forEver() waitasecond.forTime(timeout).then(() => { if (crashOnError) { reject(new Error(`Command "${humanReadableCommand}" exceeded time limit of ${timeout}ms`)); } else { console.warn(`Command "${humanReadableCommand}" exceeded time limit of ${timeout}ms but continues running`); // <- TODO: [๐Ÿฎ] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks resolve('Command exceeded time limit'); } }); } if (isVerbose) { console.info(colors__default["default"].yellow(cwd) + ' ' + colors__default["default"].green(command) + ' ' + colors__default["default"].blue(args.join(' '))); } try { const commandProcess = child_process.spawn(command, args, { cwd, shell: true }); if (isVerbose) { commandProcess.on('message', (message) => { console.info({ message }); }); } const output = []; commandProcess.stdout.on('data', (stdout) => { output.push(stdout.toString()); if (isVerbose) { console.info(stdout.toString()); } }); commandProcess.stderr.on('data', (stderr) => { output.push(stderr.toString()); if (isVerbose && stderr.toString().trim()) { console.warn(stderr.toString()); // <- TODO: [๐Ÿฎ] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks } }); const finishWithCode = (code) => { if (code !== 0) { if (crashOnError) { reject(new Error(output.join('\n').trim() || `Command "${humanReadableCommand}" exited with code ${code}`)); } else { if (isVerbose) { console.warn(`Command "${humanReadableCommand}" exited with code ${code}`); // <- TODO: [๐Ÿฎ] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks } resolve(spaceTrim.spaceTrim(output.join('\n'))); } } else { resolve(spaceTrim.spaceTrim(output.join('\n'))); } }; commandProcess.on('close', finishWithCode); commandProcess.on('exit', finishWithCode); commandProcess.on('disconnect', () => { // Note: Unexpected disconnection should always result in rejection reject(new Error(`Command "${humanReadableCommand}" disconnected`)); }); commandProcess.on('error', (error) => { if (crashOnError) { reject(new Error(`Command "${humanReadableCommand}" failed: \n${error.message}`)); } else { if (isVerbose) { console.warn(error); // <- TODO: [๐Ÿฎ] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks } resolve(spaceTrim.spaceTrim(output.join('\n'))); } }); } catch (error) { // Note: Unexpected error in sync code should always result in rejection reject(error); } }); } /** * Note: [๐ŸŸข] Code in this file should never be never released in packages that could be imported into browser environment */ /** * @@@ * * @private within the repository */ async function locateAppOnLinux({ linuxWhich, }) { try { const result = await $execCommand({ crashOnError: true, command: `which ${linuxWhich}` }); return result.trim(); } catch (error) { assertsError(error); return null; } } /** * TODO: [๐Ÿง ][โ™ฟ] Maybe export through `@promptbook/node` * Note: [๐ŸŸข] Code in this file should never be never released in packages that could be imported into browser environment */ /** * @@@ * * @public exported from `@promptbook/node` */ function $provideFilesystemForNode(options) { if (!$isRunningInNode()) { throw new EnvironmentMismatchError('Function `$provideFilesystemForNode` works only in Node.js environment'); } return { stat: promises.stat, access: promises.access, constants: promises.constants, readFile: promises.readFile, writeFile: promises.writeFile, readdir: promises.readdir, mkdir: promises.mkdir, }; } /** * Note: [๐ŸŸข] Code in this file should never be never released in packages that could be imported into browser environment */ /** * Checks if the file is executable * * @private within the repository */ async function isExecutable(path, fs) { try { await fs.access(path, fs.constants.X_OK); return true; } catch (error) { return false; } } /** * Note: Not [~๐ŸŸข~] because it is not directly dependent on `fs * TODO: [๐Ÿ–‡] What about symlinks? */ // Note: Module `userhome` has no types available, so it is imported using `require` // @see https://stackoverflow.com/questions/37000981/how-to-import-node-module-in-typescript-without-type-definitions // eslint-disable-next-line @typescript-eslint/no-var-requires const userhome = require('userhome'); /** * @@@ * * @private within the repository */ async function locateAppOnMacOs({ macOsName, }) { try { const toExec = `/Contents/MacOS/${macOsName}`; const regPath = `/Applications/${macOsName}.app` + toExec; const altPath = userhome(regPath.slice(1)); if (await isExecutable(regPath, $provideFilesystemForNode())) { return regPath; } else if (await isExecutable(altPath, $provideFilesystemForNode())) { return altPath; } const result = await $execCommand({ crashOnError: true, command: `mdfind 'kMDItemDisplayName == "${macOsName}" && kMDItemKind == Application'`, }); return result.trim() + toExec; } catch (error) { assertsError(error); return null; } } /** * TODO: [๐Ÿง ][โ™ฟ] Maybe export through `@promptbook/node` * Note: [๐ŸŸข] Code in this file should never be never released in packages that could be imported into browser environment */ /** * @@@ * * @private within the repository */ async function locateAppOnWindows({ appName, windowsSuffix, }) { try { const prefixes = [ process.env.LOCALAPPDATA, path.join(process.env.LOCALAPPDATA || '', 'Programs'), process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)'], ]; for (const prefix of prefixes) { const path = prefix + windowsSuffix; if (await isExecutable(path, $provideFilesystemForNode())) { return path; } } throw new Error(`Can not locate app ${appName} on Windows.`); } catch (error) { assertsError(error); return null; } } /** * TODO: [๐Ÿง ][โ™ฟ] Maybe export through `@promptbook/node` * Note: [๐ŸŸข] Code in this file should never be never released in packages that could be imported into browser environment */ /** * Locates an application on the system * * @private within the repository */ function locateApp(options) { if (!$isRunningInNode()) { throw new EnvironmentMismatchError('Locating apps works only in Node.js environment'); } const { appName, linuxWhich, windowsSuffix, macOsName } = options; if (process.platform === 'win32') { if (windowsSuffix) { return locateAppOnWindows({ appName, windowsSuffix }); } else { throw new Error(`${appName} is not available on Windows.`); } } else if (process.platform === 'darwin') { if (macOsName) { return locateAppOnMacOs({ macOsName }); } else { throw new Error(`${appName} is not available on macOS.`); } } else { if (linuxWhich) { return locateAppOnLinux({ linuxWhich }); } else { throw new Error(`${appName} is not available on Linux.`); } } } /** * TODO: [๐Ÿง ][โ™ฟ] Maybe export through `@promptbook/node` * Note: [๐ŸŸข] Code in this file should never be never released in packages that could be imported into browser environment */ /** * @@@ * * @private within the repository */ function locateLibreoffice() { return locateApp({ appName: 'Libreoffice', linuxWhich: 'libreoffice', windowsSuffix: '\\LibreOffice\\program\\soffice.exe', macOsName: 'LibreOffice', }); } /** * TODO: [๐Ÿง ][โ™ฟ] Maybe export through `@promptbook/node` OR `@promptbook/legacy-documents` * Note: [๐ŸŸข] Code in this file should never be never released in packages that could be imported into browser environment */ /** * @@@ * * @private within the repository */ function locatePandoc() { return locateApp({ appName: 'Pandoc', linuxWhich: 'pandoc', windowsSuffix: '\\Pandoc\\pandoc.exe', macOsName: 'Pandoc', }); } /** * TODO: [๐Ÿง ][โ™ฟ] Maybe export through `@promptbook/node` OR `@promptbook/documents` * Note: [๐ŸŸข] Code in this file should never be never released in packages that could be imported into browser environment */ /** * @@@ * * @public exported from `@promptbook/node` */ async function $provideExecutablesForNode(options) { if (!$isRunningInNode()) { throw new EnvironmentMismatchError('Function `$getScrapersForNode` works only in Node.js environment'); } return { pandocPath: (await locatePandoc()) || undefined, libreOfficePath: (await locateLibreoffice()) || undefined, // <- TODO: [๐Ÿง ] `null` vs `undefined` }; } /** * TODO: [๐Ÿง ] Allow to override the executables without need to call `locatePandoc` / `locateLibreoffice` in case of provided * Note: [๐ŸŸข] Code in this file should never be never released in packages that could be imported into browser environment */ /** * 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', 'currentDa