UNPKG

@promptbook/remote-client

Version:

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

1,533 lines (1,463 loc) โ€ข 205 kB
import spaceTrim$1, { spaceTrim } from 'spacetrim'; import { randomBytes } from 'crypto'; import { io } from 'socket.io-client'; import { SHA256 } from 'crypto-js'; import hexEncoder from 'crypto-js/enc-hex'; import { parse, unparse } from 'papaparse'; import { basename } from 'path'; // โš ๏ธ 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.101.0-4'; /** * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name */ /** * 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((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 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((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` */ /** * 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); } } /** * 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'; // <- 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; /** * 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; // <- TODO: [๐Ÿงœโ€โ™‚๏ธ] /** * Default settings for parsing and generating CSV files in Promptbook. * * @public exported from `@promptbook/core` */ Object.freeze({ delimiter: ',', quoteChar: '"', newline: '\n', skipEmptyLines: true, }); /** * API request timeout in milliseconds * Can be overridden via API_REQUEST_TIMEOUT environment variable * * @public exported from `@promptbook/core` */ parseInt(process.env.API_REQUEST_TIMEOUT || '90000'); /** * 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$1((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((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(` 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); } } /** * 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$1((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: [๐Ÿ”‚] This function is idempotent. * 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$1((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); }); } /** * Prepare pipeline on remote server * * @see https://github.com/webgptorg/promptbook/discussions/196 * * Note: This function does not validate logic of the pipeline * Note: This function acts as part of compilation process * Note: When the pipeline is already prepared, it returns the same pipeline * * @public exported from `@promptbook/remote-client` */ async function preparePipelineOnRemoteServer(pipeline, options) { const socket = await createRemoteClient(options); socket.emit('preparePipeline-request', { identification: options.identification, pipeline, } /* <- Note: [๐Ÿค›] */); const preparedPipeline = await new Promise((resolve, reject) => { socket.on('preparePipeline-response', (response) => { resolve(response.preparedPipeline); socket.disconnect(); }); socket.on('error', (error) => { reject(deserializeError(error)); socket.disconnect(); }); }); socket.disconnect(); // TODO: [๐Ÿง ] Maybe do $exportJson return preparedPipeline; } /** * TODO: [๐Ÿš] Do not return `Promise<PipelineJson>` But `PreparationTask` */ /** * All available task types * * There is is distinction between task types and section types * - Every section in markdown has its SectionType * - Some sections are tasks but other can be non-task sections * * @public exported from `@promptbook/core` */ const TaskTypes = [ 'PROMPT', 'SIMPLE', 'SCRIPT', 'DIALOG', // <- [๐Ÿ…ฑ] ]; /** * All available sections which are not tasks * * @public exported from `@promptbook/core` */ const NonTaskSectionTypes = ['EXAMPLE', 'KNOWLEDGE', 'INSTRUMENT', 'ACTION']; /** * All available section types * * There is is distinction between task types and section types * - Every section in markdown has its SectionType * - Some sections are tasks but other can be non-task sections * * @public exported from `@promptbook/core` */ const SectionTypes = [ ...TaskTypes.map((TaskType) => `${TaskType}_TASK`), ...NonTaskSectionTypes, ]; /** * Tests if given string is valid file path. * * 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; } // Normalize slashes early so heuristics can detect path-like inputs const filenameSlashes = filename.replace(/\\/g, '/'); // Reject strings that look like sentences (informational text) // Heuristic: contains multiple spaces and ends with a period, or contains typical sentence punctuation // But skip this heuristic if the string looks like a path (contains '/' or starts with a drive letter) if (filename.trim().length > 60 && // long enough to be a sentence /[.!?]/.test(filename) && // contains sentence punctuation filename.split(' ').length > 8 && // has many words !/\/|^[A-Z]:/i.test(filenameSlashes) // do NOT treat as sentence if looks like a path ) { return false; } // Absolute Unix path: /hello.txt if (/^(\/)/i.test(filenameSlashes)) { // console.log(filename, 'Absolute Unix path: /hello.txt'); return true; } // Absolute Windows path: C:/ or C:\ (allow spaces and multiple dots in filename) if (/^[A-Z]:\/.+$/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 */ const defaultDiacriticsRemovalMap = [ { base: 'A', letters: '\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F', }, { base: 'AA', letters: '\uA732' }, { base: 'AE', letters: '\u00C6\u01FC\u01E2' }, { base: 'AO', letters: '\uA734' }, { base: 'AU', letters: '\uA736' }, { base: 'AV', letters: '\uA738\uA73A' }, { base: 'AY', letters: '\uA73C' }, { base: 'B', letters: '\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181', }, { base: 'C', letters: '\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E', }, { base: 'D', letters: '\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779\u00D0', }, { base: 'DZ', letters: '\u01F1\u01C4' }, { base: 'Dz', letters: '\u01F2\u01C5' }, { base: 'E', letters: '\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E', }, { base: 'F', letters: '\u0046\u24BB\uFF26\u1E1E\u0191\uA77B' }, { base: 'G', letters: '\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E', }, { base: 'H', letters: '\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D', }, { base: 'I', letters: '\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197', }, { base: 'J', letters: '\u004A\u24BF\uFF2A\u0134\u0248' }, { base: 'K', letters: '\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2', }, { base: 'L', letters: '\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780', }, { base: 'LJ', letters: '\u01C7' }, { base: 'Lj', letters: '\u01C8' }, { base: 'M', letters: '\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C' }, { base: 'N', letters: '\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4', }, { base: 'NJ', letters: '\u01CA' }, { base: 'Nj', letters: '\u01CB' }, { base: 'O', letters: '\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C', }, { base: 'OI', letters: '\u01A2' }, { base: 'OO', letters: '\uA74E' }, { base: 'OU', letters: '\u0222' }, { base: 'OE', letters: '\u008C\u0152' }, { base: 'oe', letters: '\u009C\u0153' }, { base: 'P', letters: '\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754', }, { base: 'Q', letters: '\u0051\u24C6\uFF31\uA756\uA758\u024A' }, { base: 'R', letters: '\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782', }, { base: 'S', letters: '\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784', }, { base: 'T', letters: '\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786', }, { base: 'TZ', letters: '\uA728' }, { base: 'U', letters: '\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244', }, { base: 'V', letters: '\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245' }, { base: 'VY', letters: '\uA760' }, { base: 'W', letters: '\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72', }, { base: 'X', letters: '\u0058\u24CD\uFF38\u1E8A\u1E8C' }, { base: 'Y', letters: '\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE', }, { base: 'Z', letters: '\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762', }, { base: 'a', letters: '\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250', }, { base: 'aa', letters: '\uA733' }, { base: 'ae', letters: '\u00E6\u01FD\u01E3' }, { base: 'ao', letters: '\uA735' }, { base: 'au', letters: '\uA737' }, { base: 'av', letters: '\uA739\uA73B' }, { base: 'ay', letters: '\uA73D' }, { base: 'b', letters: '\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253', }, { base: 'c', letters: '\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184', }, { base: 'd', letters: '\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A', }, { base: 'dz', letters: '\u01F3\u01C6' }, { base: 'e', letters: '\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD', }, { base: 'f', letters: '\u0066\u24D5\uFF46\u1E1F\u0192\uA77C' }, { base: 'g', letters: '\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F', }, { base: 'h', letters: '\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265', }, { base: 'hv', letters: '\u0195' }, { base: 'i', letters: '\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131', }, { base: 'j', letters: '\u006A\u24D9\uFF4A\u0135\u01F0\u0249' }, { base: 'k', letters: '\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3', }, { base: 'l', letters: '\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747', }, { base: 'lj', letters: '\u01C9' }, { base: 'm', letters: '\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F' }, { base: 'n', letters: '\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5', }, { base: 'nj', letters: '\u01CC' }, { base: 'o', letters: '\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275', }, { base: 'oi', letters: '\u01A3' }, { base: 'ou', letters: '\u0223' }, { base: 'oo', letters: '\uA74F' }, { base: 'p', letters: '\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755', }, { base: 'q', letters: '\u0071\u24E0\uFF51\u024B\uA757\uA759' }, { base: 'r', letters: '\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783', }, { base: 's', letters: '\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B', }, { base: 't', letters: '\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787', }, { base: 'tz', letters: '\uA729' }, { base: 'u', letters: '\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289', }, { base: 'v', letters: '\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C' }, { base: 'vy', letters: '\uA761' }, { base: 'w', letters: '\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73', }, { base: 'x', letters: '\u0078\u24E7\uFF58\u1E8B\u1E8D' }, { base: 'y', letters: '\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF', }, { base: 'z', letters: '\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763', }, ]; /** * Map of letters from diacritic variant to diacritless variant * Contains lowercase and uppercase separatelly * * > "รก" => "a" * > "ฤ›" => "e" * > "ฤ‚" => "A" * > ... * * @public exported from `@promptbook/utils` */ const DIACRITIC_VARIANTS_LETTERS = {}; // tslint:disable-next-line: prefer-for-of for (let i = 0; i < defaultDiacriticsRemovalMap.length; i++) { const letters = defaultDiacriticsRemovalMap[i].letters; // tslint:disable-next-line: prefer-for-of for (let j = 0; j < letters.length; j++) { DIACRITIC_VARIANTS_LETTERS[letters[j]] = defaultDiacriticsRemovalMap[i].base; } } // <- TODO: [๐Ÿ“] Put to maker function to save execution time if not needed /* @see https://stackoverflow.com/questions/990904/remove-accents-diacritics-in-a-string-in-javascript Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ /** * Removes diacritic marks (accents) from characters in a string. * * Note: [๐Ÿ”‚] This function is idempotent. * * @param input The string containing diacritics to be normalized. * @returns The string with diacritics removed or normalized. * @public exported from `@promptbook/utils` */ function removeDiacritics(input) { /*eslint no-control-regex: "off"*/ return input.replace(/[^\u0000-\u007E]/g, (a) => { return DIACRITIC_VARIANTS_LETTERS[a] || a; }); } /** * TODO: [ะ–] Variant for cyrillic (and in general non-latin) letters */ /** * Converts a given text to kebab-case format. * * @param text The text to be converted. * @returns The kebab-case formatted string. * @example 'hello-world' * @example 'i-love-promptbook' * @public exported from `@promptbook/utils` */ function normalizeToKebabCase(text) { text = removeDiacritics(text); let charType; let lastCharType = 'OTHER'; let normalizedName = ''; for (const char of text) { let normalizedChar; if (/^[a-z]$/.test(char)) { charType = 'LOWERCASE'; normalizedChar = char; } else if (/^[A-Z]$/.test(char)) { charType = 'UPPERCASE'; normalizedChar = char.toLowerCase(); } 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.split(/-+/g).join('-'); normalizedName = normalizedName.split(/-?\/-?/g).join('/'); normalizedName = normalizedName.replace(/^-/, ''); normalizedName = normalizedName.replace(/-$/, ''); return normalizedName; } /** * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name */ /** * Creates unique name for the source * * @public exported from `@promptbook/editable` */ function knowledgeSourceContentToName(knowledgeSourceContent) { const hash = SHA256(hexEncoder.parse(JSON.stringify(knowledgeSourceContent))) // <- TODO: [๐Ÿฅฌ] Encapsulate sha256 to some private utility function .toString( /* hex */) .substring(0, 20); // <- TODO: [๐Ÿฅฌ] Make some system for hashes and ids of promptbook const semanticName = normalizeToKebabCase(knowledgeSourceContent.substring(0, 20)); const pieces = ['source', semanticName, hash].filter((piece) => piece !== ''); const name = pieces.join('-').split('--').join('-'); // <- TODO: Use MAX_FILENAME_LENGTH return name; } /** * TODO: [๐Ÿฑโ€๐Ÿ‰][๐Ÿง ] Make some smart crop NOT source-i-m-pavol-a-develop-... BUT source-i-m-pavol-a-developer-... */ /** * Parses the knowledge command * * @see `documentationUrl` for more details * @public exported from `@promptbook/editable` */ const knowledgeCommandParser = { /** * Name of the command */ name: 'KNOWLEDGE', /** * BOILERPLATE command can be used in: */ isUsedInPipelineHead: true, isUsedInPipelineTask: false, /** * Description of the KNOWLEDGE command */ description: `Tells promptbook which external knowledge to use`, /** * Link to documentation */ documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/41', /** * Example usages of the KNOWLEDGE command */ examples: [ 'KNOWLEDGE https://www.pavolhejny.com/', 'KNOWLEDGE ./hejny-cv.txt', 'KNOWLEDGE ./hejny-cv.md', 'KNOWLEDGE ./hejny-cv.pdf', 'KNOWLEDGE ./hejny-cv.docx', // <- TODO: [๐Ÿ˜ฟ] Allow ONLY files scoped in the (sub)directory NOT ../ and test it ], /** * Parses the KNOWLEDGE command */ parse(input) { const { args } = input; const knowledgeSourceContent = spaceTrim$1(args[0] || ''); if (knowledgeSourceContent === '') { throw new ParseError(`Source is not defined`); } // TODO: [main] !!4 Following checks should be applied every link in the `sourceContent` if (knowledgeSourceContent.startsWith('http://')) { throw new ParseError(`Source is not secure`); } if (!(isValidFilePath(knowledgeSourceContent) || isValidUrl(knowledgeSourceContent))) { throw new ParseError(`Source not valid`); } if (knowledgeSourceContent.startsWith('../') || knowledgeSourceContent.startsWith('/') || /^[A-Z]:[\\/]+/i.test(knowledgeSourceContent)) { throw new ParseError(`Source cannot be outside of the .book.md folder`); } return { type: 'KNOWLEDGE', knowledgeSourceContent, }; }, /** * Apply the KNOWLEDGE command to the `pipelineJson` * * Note: `$` is used to indicate that this function mutates given `pipelineJson` */ $applyToPipelineJson(command, $pipelineJson) { const { knowledgeSourceContent } = command; $pipelineJson.knowledgeSources.push({ name: knowledgeSourceContentToName(knowledgeSourceContent), knowledgeSourceContent, }); }, /** * Converts the KNOWLEDGE command back to string * * Note: This is used in `pipelineJsonToString` utility */ stringify(command) { return `---`; // <- TODO: [๐Ÿ›‹] Implement }, /** * Reads the KNOWLEDGE command from the `PipelineJson` * * Note: This is used in `pipelineJsonToString` utility */ takeFromPipelineJson(pipelineJson) { throw new NotYetImplementedError(`[๐Ÿ›‹] Not implemented yet`); // <- TODO: [๐Ÿ›‹] Implement }, }; /** * Note: [โ›ฑ] There are two types of KNOWLEDGE commands *...(read more in [โ›ฑ])* */ /** * Parses the section command * * @see `documentationUrl` for more details * @public exported from `@promptbook/editable` */ const sectionCommandParser = { /** * Name of the command */ name: 'SECTION', /** * Aliases for the SECTION command */ aliasNames: [ 'PROMPT', 'SIMPLE', 'SCRIPT', 'DIALOG', 'SAMPLE', 'EXAMPLE', 'KNOWLEDGE', 'INSTRUMENT', 'ACTION', // <- Note: [โ›ฑ] ], /** * Aliases for the SECTION command */ deprecatedNames: ['TEMPLATE', 'BLOCK', 'EXECUTE'], /** * BOILERPLATE command can be used in: */ isUsedInPipelineHead: false, isUsedInPipelineTask: true, /** * Description of the SECTION command */ description: `Defines the purpose of the markdown section - if its a task and which type or something else`, /** * Link to documentation */ documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/64', /** * Example usages of the SECTION command */ examples: [ // Short form: 'PROMPT', 'SIMPLE', 'SCRIPT', 'DIALOG', // <- [๐Ÿ…ฑ] 'EXAMPLE', 'KNOWLEDGE', 'INSTRUMENT', 'ACTION', // ----------------- // Recommended (reversed) form: 'PROMPT SECTION', 'SIMPLE SECTION', 'SCRIPT SECTION', 'DIALOG SECTION', // <- [๐Ÿ…ฑ] 'EXAMPLE SECTION', 'KNOWLEDGE SECTION', 'INSTRUMENT SECTION', 'ACTION SECTION', // ----------------- // Standard form: 'SECTION PROMPT', 'SECTION SIMPLE', 'SECTION SCRIPT', 'SECTION DIALOG', // <- [๐Ÿ…ฑ] 'SECTION EXAMPLE', 'SECTION KNOWLEDGE', 'SECTION INSTRUMENT', 'SECTION ACTION', ], // TODO: [โ™“๏ธ] order: -10 /* <- Note: Putting before other commands */ /** * Parses the SECTION command */ parse(input) { let { normalized } = input; normalized = normalized.split('SAMPLE').join('EXAMPLE'); normalized = normalized.split('EXECUTE_').join(''); normalized = normalized.split('DIALOGUE').join('DIALOG'); const taskTypes = SectionTypes.filter((sectionType) => normalized.includes(sectionType.split('_TASK').join(''))); if (taskTypes.length !== 1) { throw new ParseError(spaceTrim$1((block) => ` Unknown section type "${normalized}" Supported section types are: ${block(SectionTypes.join(', '))} `)); } const taskType = taskTypes[0]; return { type: 'SECTION', taskType, }; }, /** * Apply the SECTION command to the `pipelineJson` * * Note: `$` is used to indicate that this function mutates given `taskJson` */ $applyToTaskJson(command, $taskJson, $pipelineJson) { if ($taskJson.isSectionTypeSet === true) { throw new ParseError(spaceTrim$1(` Section type is already defined in the section. It can be defined only once. `)); } $taskJson.isSectionTypeSet = true; // TODO: [๐Ÿง][๐Ÿ’ฉ] Rearrange better - but at bottom and unwrap from function const expectResultingParameterName = () => { if ($taskJson.resultingParameterName) { return; } throw new ParseError(`Task section and example section must end with return statement -> {parameterName}`); }; if ($taskJson.content === undefined) { throw new UnexpectedError(`Content is missing in the taskJson - probably commands are applied in wrong order`); } if (command.taskType === 'EXAMPLE') { expectResultingParameterName(); const parameter = $pipelineJson.parameters.find((param) => param.name === $taskJson.resultingParameterName); if (parameter === undefined) { // TODO: !!6 Change to logic error for higher level abstraction of chatbot to work throw new ParseError(`Parameter \`{${$taskJson.resultingParameterName}}\` is not defined so can not define example value of it`); } parameter.exampleValues = parameter.exampleValues || []; parameter.exampleValues.push($taskJson.content); $taskJson.isTask = false; return; } if (command.taskType === 'KNOWLEDGE') { knowledgeCommandParser.$applyToPipelineJson({ type: 'KNOWLEDGE', knowledgeSourceContent: $taskJson.content, // <- TODO: [๐Ÿ][main] !!3 Work with KNOWLEDGE which not referring to the source file or website, but its content itself }, $pipelineJson); $taskJson.isTask = false; return; } if (command.taskType === 'ACTION') { console.error(new NotYetImplementedError('Actions are not implemented yet')); $taskJson.isTask = false; return; } if (command.taskType === 'INSTRUMENT') { console.error(new NotYetImplementedError('Instruments are not implemented yet')); $taskJson.isTask = false; return; } expectResultingParameterName(); $taskJson.taskType = command.taskType; $taskJson.isTask = true; }, /** * Converts the SECTION command back to string * * Note: This is used in `pipelineJsonToString` utility */ stringify(command) { return `---`; // <- TODO: [๐Ÿ›‹] Implement }, /** * Reads the SECTION command from the `TaskJson` * * Note: This is used in `pipelineJsonToString` utility */ takeFromTaskJson($taskJson) { throw new NotYetImplementedError(`[๐Ÿ›‹] Not implemented yet`); // <- TODO: [๐Ÿ›‹] Implement }, }; /** * Note: [โ›ฑ] There are two types of KNOWLEDGE, ACTION and INSTRUMENT commands: * 1) There are commands `KNOWLEDGE`, `ACTION` and `INSTRUMENT` used in the pipeline head, they just define the knowledge, action or instrument as single line after the command * - KNOWLEDGE Look at https://en.wikipedia.org/wiki/Artificial_intelligence * 2) `KNOWLEDGE SECTION` which has short form `KNOWLEDGE` is used in the sectiom, does not refer the line itself, but the content of the section block * - KNOWLEDGE SECTION * * ``` * Look at https://en.wikipedia.org/wiki/Artificial_intelligence * ``` */ /** * Parses the boilerplate command * * Note: @@ This command is used as boilerplate for new commands - it should NOT be used in any `.book` file * * @see `documentationUrl` for more details * @private within the commands folder */ const boilerplateCommandParser = { /** * Name of the command */ name: 'BOILERPLATE', /** * Aliases for the BOILERPLATE command */ aliasNames: ['BP'], /** * BOILERPLATE command can be used in: */ isUsedInPipelineHead: true, isUsedInPipelineTask: true, /** * Description of the BOILERPLATE command */ description: `@@`, /** * Link to documentation */ documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/@@', /** * Example usages of the BOILERPLATE command */ examples: ['BOILERPLATE foo', 'BOILERPLATE bar', 'BP foo', 'BP bar'], /** * Parses the BOILERPLATE command */ parse(input) { const { args } = input; if (args.length !== 1) { throw new ParseError(`BOILERPLATE command requires exactly one argument`); } const value = args[0].toLowerCase(); if (value.includes('brr')) { throw new ParseError(`BOILERPLATE value can not contain brr`); } return { type: 'BOILERPLATE', value, }; }, /** * Apply the BOILERPLATE command to the `pipelineJson` * * Note: `$` is used to indicate that this function mutates given `pipelineJson` */ $applyToPipelineJson(command, $pipelineJson) { throw new ParseError(`BOILERPLATE command is only for testing purposes and should not be used in the .book.md file`); }, /** * Apply the BOILERPLATE command to the `pipelineJson` * * Note: `$` is used to indicate that this function mutates given `taskJson` */ $applyToTaskJson(command, $taskJson, $pipelineJson) { throw new ParseError(`BOILERPLATE command is only for testing purposes and should not be used in the .book.md file`); }, /** * Converts the BOILERPLATE command back to string * * Note: This is used in `pipelineJsonToString` utility */ stringify(command) { return `---`; // <- TODO: [๐Ÿ›‹] Implement }, /** * Reads the BOILERPLATE command from the `PipelineJson` * * Note: This is used in `pipelineJsonToString` utility */ takeFromPipelineJson(pipelineJson) { throw new ParseError(`BOILERPLATE command is only for testing purposes and should not be used in the .book.md file`); }, /** * Reads the BOILERPLATE command from the `TaskJson` * * Note: This is used in `pipelineJsonToString` utility */ takeFromTaskJson($taskJson) { throw new ParseError(`BOILERPLATE command is only for testing purposes and should not be used in the .book.md file`); }, }; /** * Tests if given string is valid semantic version * * Note: There are two similar 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 similar 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; } /** * Parses the BOOK_VERSION command * * @see `documentationUrl` for more details * @public exported from `@promptbook/editable` */ const bookVersionCommandParser = { /** * Name of the command */ name: 'BOOK_VERSION', aliasNames: ['PTBK_VERSION', 'PROMPTBOOK_VERSION', 'BOOK'], /** * BOILERPLATE command can be used in: *