@promptbook/wizard
Version:
Promptbook: Run AI apps in plain human language across multiple models and platforms
1,587 lines (1,506 loc) โข 693 kB
JavaScript
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