UNPKG

@promptbook/wizard

Version:

Promptbook: Run AI apps in plain human language across multiple models and platforms

1,587 lines (1,506 loc) โ€ข 693 kB
import spaceTrim, { spaceTrim as spaceTrim$1 } from 'spacetrim'; import { randomBytes } from 'crypto'; import { io } from 'socket.io-client'; import Anthropic from '@anthropic-ai/sdk'; import Bottleneck from 'bottleneck'; import colors from 'colors'; import { OpenAIClient, AzureKeyCredential } from '@azure/openai'; import OpenAI from 'openai'; import { mkdir, rm, readFile, readdir, rename, rmdir, stat, access, constants, writeFile, unlink } from 'fs/promises'; import { spawn } from 'child_process'; import { forTime } from 'waitasecond'; import { SHA256 } from 'crypto-js'; import hexEncoder from 'crypto-js/enc-hex'; import { basename, join, dirname, relative } from 'path'; import { format } from 'prettier'; import parserHtml from 'prettier/parser-html'; import { Subject } from 'rxjs'; import sha256 from 'crypto-js/sha256'; import { lookup, extension } from 'mime-types'; import { parse, unparse } from 'papaparse'; import { Readability } from '@mozilla/readability'; import { JSDOM } from 'jsdom'; import { Converter } from 'showdown'; import * as dotenv from 'dotenv'; import JSZip from 'jszip'; // โš ๏ธ 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.100.0-28'; /** * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name */ /** * Available remote servers for the Promptbook * * @public exported from `@promptbook/core` */ const REMOTE_SERVER_URLS = [ { title: 'Promptbook', description: `Servers of Promptbook.studio`, owner: 'AI Web, LLC <legal@ptbk.io> (https://www.ptbk.io/)', isAnonymousModeAllowed: true, urls: [ 'https://promptbook.s5.ptbk.io/', // Note: Servers 1-4 are not running ], }, /* Note: Working on older version of Promptbook and not supported anymore { title: 'Pavol Promptbook Server', description: `Personal server of Pavol Hejnรฝ with simple testing server, DO NOT USE IT FOR PRODUCTION`, owner: 'Pavol Hejnรฝ <pavol@ptbk.io> (https://www.pavolhejny.com/)', isAnonymousModeAllowed: true, urls: ['https://api.pavolhejny.com/promptbook'], }, */ ]; /** * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name */ /** * Returns the same value that is passed as argument. * No side effects. * * Note: It can be useful 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'; // <- 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 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 /** * Threshold value that determines when a dataset is considered "big" * and may require special handling or optimizations * * For example, when error occurs in one item of the big dataset, it will not fail the whole pipeline * * @public exported from `@promptbook/core` */ const BIG_DATASET_TRESHOLD = 50; /** * Placeholder text used to represent a placeholder value of failed operation * * @public exported from `@promptbook/core` */ const FAILED_VALUE_PLACEHOLDER = '!?'; /** * Warning message for the automatically generated sections of `.env` files * * @private within the repository */ const GENERATOR_WARNING_IN_ENV = `Note: Added by Promptbook`; // <- 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; /** * Timeout for the connections in milliseconds * * @private within the repository - too low-level in comparison with other `MAX_...` */ const CONNECTION_TIMEOUT_MS = 7 * 1000; // <- TODO: [โณ] Standardize timeouts, Make DEFAULT_TIMEOUT_MS as global constant /** * How many times to retry the connections * * @private within the repository - too low-level in comparison with other `MAX_...` */ const CONNECTION_RETRIES_LIMIT = 5; /** * 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 = 7; // <- TODO: [๐Ÿคนโ€โ™‚๏ธ] // <- TODO: [๐Ÿ] /** * 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'; /* TODO: [๐ŸŒƒ] /** * Id of application for the wizard when using remote server * * @public exported from `@promptbook/core` * / ex-port const WIZARD_APP_ID: string_app_id = 'wizard'; */ /** * 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`; /** * Default remote server URL for the Promptbook * * @public exported from `@promptbook/core` */ const DEFAULT_REMOTE_SERVER_URL = REMOTE_SERVER_URLS[0].urls[0]; // <- TODO: [๐Ÿงœโ€โ™‚๏ธ] /** * Default settings for parsing and generating CSV files in Promptbook. * * @public exported from `@promptbook/core` */ const DEFAULT_CSV_SETTINGS = Object.freeze({ delimiter: ',', quoteChar: '"', newline: '\n', skipEmptyLines: true, }); /** * Controls whether verbose logging is enabled by default throughout the application. * * @public exported from `@promptbook/core` */ let DEFAULT_IS_VERBOSE = false; /** * Controls whether auto-installation of dependencies is enabled by default. * * @public exported from `@promptbook/core` */ const DEFAULT_IS_AUTO_INSTALLED = false; /** * Default rate limits (requests per minute) * * Note: Adjust based on the provider tier you are have * * @public exported from `@promptbook/core` */ const DEFAULT_MAX_REQUESTS_PER_MINUTE = 60; /** * Indicates whether pipeline logic validation is enabled. When true, the pipeline logic is checked for consistency. * * @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 */ /** * 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 */ /** * 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((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$1((block) => ` ${block(message)} Note: This error should not happen. It's probably 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$1(` 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); } /** * 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((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((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 omitted continue; } checkSerializableAsJson({ name: `${name}.${subName}`, value: subValue, message }); } try { JSON.stringify(value); // <- TODO: [0] } catch (error) { assertsError(error); throw new UnexpectedError(spaceTrim((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((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 */ /** * Creates a deep clone of the given object * * Note: This method only works for objects that are fully serializable to JSON and do not contain functions, Dates, or special types. * * @param objectValue The object to clone. * @returns A deep, writable clone of the input object. * @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 */ // <- TODO: Maybe do better levels of trust /** * How is the model provider important? * * @public exported from `@promptbook/core` */ const MODEL_ORDERS = { /** * Top-tier models, e.g. OpenAI, Anthropic,... */ TOP_TIER: 333, /** * Mid-tier models, e.g. Llama, Mistral, etc. */ NORMAL: 100, /** * Low-tier models, e.g. Phi, Tiny, etc. */ LOW_TIER: 0, }; /** * 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'; /** * Nonce which is used as string which is not occurring in normal text * * @private within the repository */ const SALT_NONCE = 'ptbkghhewbvruets21t54et5'; /** * Placeholder value indicating a parameter is missing its value. * * @private within the repository */ const RESERVED_PARAMETER_MISSING_VALUE = 'MISSING-' + REPLACING_NONCE; /** * Placeholder value indicating a parameter is restricted and cannot be used directly. * * @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 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$1((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); } } /** * Safely retrieves the global scope object (window in browser, global in Node.js) * regardless of the JavaScript environment in which the code is running * * Note: `$` is used to indicate that this function is not a pure function - it access global scope * * @private internal function of `$Register` */ function $getGlobalScope() { return Function('return this')(); } /** * Normalizes a text string to SCREAMING_CASE (all uppercase with underscores). * * @param text The text string to be converted to SCREAMING_CASE format. * @returns The normalized text in SCREAMING_CASE format. * @example 'HELLO_WORLD' * @example 'I_LOVE_PROMPTBOOK' * @public exported from `@promptbook/utils` */ function normalizeTo_SCREAMING_CASE(text) { let charType; let lastCharType = 'OTHER'; let normalizedName = ''; for (const char of text) { let normalizedChar; if (/^[a-z]$/.test(char)) { charType = 'LOWERCASE'; normalizedChar = char.toUpperCase(); } else if (/^[A-Z]$/.test(char)) { charType = 'UPPERCASE'; normalizedChar = char; } else if (/^[0-9]$/.test(char)) { charType = 'NUMBER'; normalizedChar = char; } else { charType = 'OTHER'; normalizedChar = '_'; } if (charType !== lastCharType && !(lastCharType === 'UPPERCASE' && charType === 'LOWERCASE') && !(lastCharType === 'NUMBER') && !(charType === 'NUMBER')) { normalizedName += '_'; } normalizedName += normalizedChar; lastCharType = charType; } normalizedName = normalizedName.replace(/_+/g, '_'); normalizedName = normalizedName.replace(/_?\/_?/g, '/'); normalizedName = normalizedName.replace(/^_/, ''); normalizedName = normalizedName.replace(/_$/, ''); return normalizedName; } /** * TODO: Tests * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: 'Moje tabule' })).toEqual('/VtG7sR9rRJqwNEdM2/Moje tabule'); * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: 'ฤ›ลกฤล™ลพลพรฝรกรญรบลฏ' })).toEqual('/VtG7sR9rRJqwNEdM2/escrzyaieuu'); * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: ' ahoj ' })).toEqual('/VtG7sR9rRJqwNEdM2/ahoj'); * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: ' ahoj_ahojAhoj ahoj ' })).toEqual('/VtG7sR9rRJqwNEdM2/ahoj-ahoj-ahoj-ahoj'); * TODO: [๐ŸŒบ] Use some intermediate util splitWords */ /** * Normalizes a text string to snake_case format. * * @param text The text string to be converted to snake_case format. * @returns The normalized text in snake_case format. * @example 'hello_world' * @example 'i_love_promptbook' * @public exported from `@promptbook/utils` */ function normalizeTo_snake_case(text) { return normalizeTo_SCREAMING_CASE(text).toLowerCase(); } /** * Global registry for storing and managing registered entities of a given type. * * Note: `$` is used to indicate that this function is not a pure function - it accesses and adds variables in global scope. * * @private internal utility, exported are only singleton instances of this class */ class $Register { constructor(registerName) { this.registerName = registerName; const storageName = `_promptbook_${normalizeTo_snake_case(registerName)}`; const globalScope = $getGlobalScope(); if (globalScope[storageName] === undefined) { globalScope[storageName] = []; } else if (!Array.isArray(globalScope[storageName])) { throw new UnexpectedError(`Expected (global) ${storageName} to be an array, but got ${typeof globalScope[storageName]}`); } this.storage = globalScope[storageName]; } list() { // <- TODO: ReadonlyDeep<ReadonlyArray<TRegistered>> return this.storage; } register(registered) { const { packageName, className } = registered; const existingRegistrationIndex = this.storage.findIndex((item) => item.packageName === packageName && item.className === className); const existingRegistration = this.storage[existingRegistrationIndex]; if (!existingRegistration) { this.storage.push(registered); } else { this.storage[existingRegistrationIndex] = registered; } return { registerName: this.registerName, packageName, className, get isDestroyed() { return false; }, destroy() { throw new NotYetImplementedError(`Registration to ${this.registerName} is permanent in this version of Promptbook`); }, }; } } /** * Register for LLM tools metadata. * * Note: `$` is used to indicate that this interacts with the global scope * @singleton Only one instance of each register is created per build, but there can be more instances across different builds or environments. * @public exported from `@promptbook/core` */ const $llmToolsMetadataRegister = new $Register('llm_tools_metadata'); /** * TODO: [ยฎ] DRY Register logic */ /** * Registration of LLM provider metadata * * Warning: This is not useful for the end user, it is just a side effect of the mechanism that handles all available LLM tools * * @public exported from `@promptbook/core` * @public exported from `@promptbook/wizard` * @public exported from `@promptbook/cli` */ const _AnthropicClaudeMetadataRegistration = $llmToolsMetadataRegister.register({ title: 'Anthropic Claude', packageName: '@promptbook/anthropic-claude', className: 'AnthropicClaudeExecutionTools', envVariables: ['ANTHROPIC_CLAUDE_API_KEY'], trustLevel: 'CLOSED', order: MODEL_ORDERS.TOP_TIER, getBoilerplateConfiguration() { return { title: 'Anthropic Claude', packageName: '@promptbook/anthropic-claude', className: 'AnthropicClaudeExecutionTools', options: { apiKey: 'sk-ant-api03-', isProxied: true, remoteServerUrl: DEFAULT_REMOTE_SERVER_URL, maxRequestsPerMinute: DEFAULT_MAX_REQUESTS_PER_MINUTE, }, }; }, createConfigurationFromEnv(env) { // Note: Note using `process.env` BUT `env` to pass in the environment variables dynamically if (typeof env.ANTHROPIC_CLAUDE_API_KEY === 'string') { return { title: 'Claude (from env)', packageName: '@promptbook/anthropic-claude', className: 'AnthropicClaudeExecutionTools', options: { apiKey: env.ANTHROPIC_CLAUDE_API_KEY, }, }; } return null; }, }); /** * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name */ /** * Register for LLM tools. * * Note: `$` is used to indicate that this interacts with the global scope * @singleton Only one instance of each register is created per build, but there can be more instances across different builds or environments. * @public exported from `@promptbook/core` */ const $llmToolsRegister = new $Register('llm_execution_tools_constructors'); /** * TODO: [ยฎ] DRY Register logic */ /** * 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); } } /** * 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); } } /** * This error indicates that the pipeline collection cannot be properly 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$1((block) => ` ${block(message)} Note: You have probably 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 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` */ /** * 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 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 similar char conflicts */)}`; Object.setPrototypeOf(this, PipelineExecutionError.prototype); } } /** * TODO: [๐Ÿง ][๐ŸŒ‚] Add id to all errors */ /** * 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); } } /** * 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); } } /** * 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 */ /** * Deserializes the error object * * @public exported from `@promptbook/utils` */ function deserializeError(error) { const { name, stack, id } = error; // Added id let { message } = error; let ErrorClass = ALL_ERRORS[error.name]; if (ErrorClass === undefined) { ErrorClass = Error; message = `${name}: ${message}`; } if (stack !== undefined && stack !== '') { message = spaceTrim((block) => ` ${block(message)} Original stack trace: ${block(stack || '')} `); } const deserializedError = new ErrorClass(message); deserializedError.id = id; // Assign id to the error object return deserializedError; } /** * Tests if given string is valid URL. * * Note: Dataurl are considered perfectly valid. * Note: There are two similar 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; } } /** * Creates a connection to the remote proxy server. * * Note: This function creates a connection to the remote server and returns a socket but responsibility of closing the connection is on the caller * * @private internal utility function */ async function createRemoteClient(options) { const { remoteServerUrl } = options; if (!isValidUrl(remoteServerUrl)) { throw new Error(`Invalid \`remoteServerUrl\`: "${remoteServerUrl}"`); } const remoteServerUrlParsed = new URL(remoteServerUrl); if (remoteServerUrlParsed.pathname !== '/' && remoteServerUrlParsed.pathname !== '') { remoteServerUrlParsed.pathname = '/'; throw new Error(spaceTrim((block) => ` Remote server requires root url \`/\` You have provided \`remoteServerUrl\`: ${block(remoteServerUrl)} But something like this is expected: ${block(remoteServerUrlParsed.href)} Note: If you need to run multiple services on the same server, use 3rd or 4th degree subdomain `)); } return new Promise((resolve, reject) => { const socket = io(remoteServerUrl, { retries: CONNECTION_RETRIES_LIMIT, timeout: CONNECTION_TIMEOUT_MS, path: '/socket.io', transports: ['polling', 'websocket' /*, <- TODO: [๐ŸŒฌ] Allow to pass `transports`, add 'webtransport' */], }); // console.log('Connecting to', this.options.remoteServerUrl.href, { socket }); socket.on('connect', () => { resolve(socket); }); // TODO: [๐Ÿ’ฉ] Better timeout handling setTimeout(() => { reject(new Error(`Timeout while connecting to ${remoteServerUrl}`)); }, CONNECTION_TIMEOUT_MS); }); } /** * Just says that the variable is not used but should be kept * No side effects. * * Note: It can be useful for: * * 1) Suppressing eager optimization of unused imports * 2) Suppressing eslint errors of unused variables in the tests * 3) Keeping the type of the variable for type testing * * @param value any values * @returns void * @private within the repository */ function keepUnused(...valuesToKeep) { } /** * Remote server is a proxy server that uses its execution tools internally and exposes the executor interface externally. * * You can simply use `RemoteExecutionTools` on client-side javascript and connect to your remote server. * This is useful to make all logic on browser side but not expose your API keys or no need to use customer's GPU. * * @see https://github.com/webgptorg/promptbook#remote-server * @public exported from `@promptbook/remote-client` */ class RemoteLlmExecutionTools { /* <- TODO: [๐Ÿš] `, Destroyable` */ constructor(options) { this.options = options; } get title() { // TODO: [๐Ÿง ] Maybe fetch title+description from the remote server (as well as if model methods are defined) return 'Promptbook remote server'; } get description() { return `Models from Promptbook remote server ${this.options.remoteServerUrl}`; } /** * Check the configuration of all execution tools */ async checkConfiguration() { const socket = await createRemoteClient(this.options); socket.disconnect(); // TODO: [main] !!3 Check version of the remote server and compatibility // TODO: [๐ŸŽ] Send checkConfiguration } /** * List all available models that can be used */ async listModels() { // TODO: [๐Ÿ‘’] Listing models (and checking configuration) probably should go through REST API not Socket.io const socket = await createRemoteClient(this.options); socket.emit('listModels-request', { identification: this.options.identification, } /* <- Note: [๐Ÿค›] */); const promptResult = await new Promise((resolve, reject) => { socket.on('listModels-response', (response) => { resolve(response.models); socket.disconnect(); }); socket.on('error', (error) => { reject(deserializeError(error)); socket.disconnect(); }); }); socket.disconnect(); return promptResult; } /** * Calls remote proxy server to use a chat model */ callChatModel(prompt) { if (this.options.isVerbose) { console.info(`๐Ÿ–‹ Remote callChatModel call`); } return /* not await */ this.callCommonModel(prompt); } /** * Calls remote proxy server to use a completion model */ callCompletionModel(prompt) { if (this.options.isVerbose) { console.info(`๐Ÿ’ฌ Remote callCompletionModel call`); } return /* not await */ this.callCommonModel(prompt); } /** * Calls remote proxy server to use a embedding model */ callEmbeddingModel(prompt) { if (this.options.isVerbose) { console.info(`๐Ÿ’ฌ Remote callEmbeddingModel call`); } return /* not await */ this.callCommonModel(prompt); } // <- Note: [๐Ÿค–] callXxxModel /** * Calls remote proxy server to use both completion or chat model */ async callCommonModel(prompt) { const socket = await createRemoteClient(this.options); socket.emit('prompt-request', { identification: this.options.identification, prompt, } /* <- Note: [๐Ÿค›] */); const promptResult = await new Promise((resolve, reject) => { socket.on('prompt-response', (response) => { resolve(response.promptResult); socket.disconnect(); }); socket.on('error', (error) => { reject(deserializeError(error)); socket.disconnect(); }); }); socket.disconnect(); return promptResult; } } /** * TODO: Maybe use `$exportJson` * TODO: [๐Ÿง ][๐Ÿ›] Maybe not `isAnonymous: boolean` BUT `mode: 'ANONYMOUS'|'COLLECTION'` * TODO: [๐Ÿ“] Allow to list compatible models with each variant * TODO: [๐Ÿ—ฏ] RemoteLlmExecutionTools should extend Destroyable and implement IDestroyable * TODO: [๐Ÿง ][๐ŸŒฐ] Allow to pass `title` for tracking purposes * TODO: [๐Ÿง ] Maybe remove `@promptbook/remote-client` and just use `@promptbook/core` */ /** * Simple wrapper `new Date().toISOString()` * * Note: `$` is used to indicate that this function is not a pure function - it is not deterministic because it depends on the current time * * @returns string_date branded type * @public exported from `@promptbook/utils` */ function $getCurrentDate() { return new Date().toISOString(); } /** * Format either small or big number * * @public exported from `@promptbook/utils` */ function numberToString(value) { if (value === 0) { return '0'; } else if (Number.isNaN(value)) { return VALUE_STRINGS.nan; } else if (value === Infinity) { return VALUE_STRINGS.infinity; } else if (value === -Infinity) { return VALUE_STRINGS.negativeInfinity; } for (let exponent = 0; exponent < 15; exponent++) { const factor = 10 ** exponent; const valueRounded = Math.round(value * factor) / factor; if (Math.abs(value - valueRounded) / value < SMALL_NUMBER) { return valueRounded.toFixed(exponent); } } return value.toString(); } /** * Function `valueToString` will convert the given value to string * This is useful and used in the `templateParameters` function * * Note: This function is not just calling `toString` method * It's more complex and can handle this conversion specifically for LLM models * See `VALUE_STRINGS` * * Note: There are 2 similar functions * - `valueToString` converts value to string for LLM models as human-readable string * - `asSerializable` converts value to string to preserve full information to be able to convert it back * * @public exported from `@promptbook/utils` */ function valueToString(value) { try { if (value === '') { return VALUE_STRINGS.empty; } else i