@promptbook/remote-client
Version:
Promptbook: Create persistent AI agents that turn your company's scattered knowledge into action
7,261 lines • 274 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('spacetrim'), require('crypto-js'), require('papaparse'), require('crypto-js/enc-hex'), require('path')) :
typeof define === 'function' && define.amd ? define(['exports', 'spacetrim', 'crypto-js', 'papaparse', 'crypto-js/enc-hex', 'path'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["promptbook-remote-client"] = {}, global.spacetrim, global.CryptoJS, global.papaparse, global.hexEncoder, global.path));
})(this, (function (exports, spacetrim, CryptoJS, papaparse, hexEncoder, path) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var CryptoJS__default = /*#__PURE__*/_interopDefaultLegacy(CryptoJS);
var papaparse__default = /*#__PURE__*/_interopDefaultLegacy(papaparse);
var hexEncoder__default = /*#__PURE__*/_interopDefaultLegacy(hexEncoder);
// ⚠️ 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 = '2.0.0';
/**
* The version of the Promptbook engine
*
* @generated
* @see https://github.com/webgptorg/promptbook
*/
const PROMPTBOOK_ENGINE_VERSION = '0.114.0-50';
/**
* 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);
}
}
/**
* Signals that the requested operation could not be completed because the target already exists.
*
* @public exported from `@promptbook/core`
*/
class ConflictError extends Error {
constructor(message) {
super(message);
this.name = 'ConflictError';
Object.setPrototypeOf(this, ConflictError.prototype);
}
}
/**
* This error indicates error from the database
*
* @public exported from `@promptbook/core`
*/
class DatabaseError extends Error {
constructor(message) {
super(message);
this.name = 'DatabaseError';
Object.setPrototypeOf(this, DatabaseError.prototype);
}
}
// TODO: [🐱🚀] Explain that NotFoundError ([🐱🚀] and other specific errors) has priority over DatabaseError in some contexts
/**
* 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
*
* 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
*
* @public exported from `@promptbook/core`
*/
class ExpectError extends Error {
constructor(message) {
super(message);
this.name = 'ExpectError';
Object.setPrototypeOf(this, ExpectError.prototype);
}
}
/**
* This error indicates that the promptbook can not retrieve knowledge from external sources
*
* @public exported from `@promptbook/core`
*/
class KnowledgeScrapeError extends Error {
constructor(message) {
super(message);
this.name = 'KnowledgeScrapeError';
Object.setPrototypeOf(this, KnowledgeScrapeError.prototype);
}
}
/**
* This error type indicates that some limit was reached
*
* @public exported from `@promptbook/core`
*/
class LimitReachedError extends Error {
constructor(message) {
super(message);
this.name = 'LimitReachedError';
Object.setPrototypeOf(this, LimitReachedError.prototype);
}
}
/**
* This error type indicates that some tools are missing for pipeline execution or preparation
*
* @public exported from `@promptbook/core`
*/
class MissingToolsError extends Error {
constructor(message) {
super(spacetrim.spaceTrim((block) => `
${block(message)}
Note: You have probably forgot to provide some tools for pipeline execution or preparation
`));
this.name = 'MissingToolsError';
Object.setPrototypeOf(this, MissingToolsError.prototype);
}
}
/**
* This error indicates that promptbook operation is not allowed
*
* @public exported from `@promptbook/core`
*/
class NotAllowed extends Error {
constructor(message) {
super(message);
this.name = 'NotAllowed';
Object.setPrototypeOf(this, NotAllowed.prototype);
}
}
/**
* This error indicates that promptbook not found in the collection
*
* @public exported from `@promptbook/core`
*/
class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = 'NotFoundError';
Object.setPrototypeOf(this, NotFoundError.prototype);
}
}
/**
* This error type indicates that some part of the code is not implemented yet
*
* @public exported from `@promptbook/core`
*/
class NotYetImplementedError extends Error {
constructor(message) {
super(spacetrim.spaceTrim((block) => `
${block(message)}
Note: This feature is not implemented yet but it will be soon.
If you want speed up the implementation or just read more, look here:
https://github.com/webgptorg/promptbook
Or contact us on pavol@ptbk.io
`));
this.name = 'NotYetImplementedError';
Object.setPrototypeOf(this, NotYetImplementedError.prototype);
}
}
/**
* This error indicates that the promptbook in a markdown format cannot be parsed into a valid promptbook object
*
* @public exported from `@promptbook/core`
*/
class ParseError extends Error {
constructor(message) {
super(message);
this.name = 'ParseError';
Object.setPrototypeOf(this, ParseError.prototype);
}
}
// TODO: Maybe split `ParseError` and `ApplyError`
/**
* Generates random token
*
* Note: `$` is used to indicate that this function is not a pure function - it is not deterministic
* Note: This function is cryptographically secure (it uses crypto.randomBytes internally)
*
* @returns secure random token
*
* @private internal helper function
*/
function $randomToken(randomness) {
return CryptoJS__default["default"].lib.WordArray.random(randomness).toString(CryptoJS__default["default"].enc.Hex);
}
// TODO: [🤶] Maybe export through `@promptbook/utils` or `@promptbook/random` package
// 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);
}
}
/**
* Trims string from all 4 sides
*
* Note: This is a re-exported function from the `spacetrim` package which is
* Developed by same author @hejny as this package
*
* @see https://github.com/hejny/spacetrim#usage
*
* @public exported from `@promptbook/utils`
*/
const spaceTrim = spacetrim.spaceTrim;
/**
* Class implementing take chain.
*
* @de
*
* @private util of `@promptbook/color`
*/
class TakeChain {
constructor(value) {
this.value = value;
}
then(callback) {
const newValue = callback(this.value);
return take(newValue);
}
}
/**
* A function that takes an initial value and returns a proxy object with chainable methods.
*
* @param {*} initialValue - The initial value.
* @returns {Proxy<WithTake<TValue>>} - A proxy object with a `take` method.
* @deprecated [🤡] Use some better functional library instead of `TakeChain`
*
* @private util of `@promptbook/color`
*/
function take(initialValue) {
if (initialValue instanceof TakeChain) {
return initialValue;
}
return new Proxy(new TakeChain(initialValue), {
get(target, property, receiver) {
if (Reflect.has(target, property)) {
return Reflect.get(target, property, receiver);
}
else if (Reflect.has(initialValue, property)) {
return Reflect.get(initialValue, property, receiver);
}
else {
return undefined;
}
},
});
}
/**
* Validates that a channel value is a valid number within the range of 0 to 255.
* Throws an error if the value is not valid.
*
* @param channelName - The name of the channel being validated.
* @param value - The value of the channel to validate.
* @throws Will throw an error if the value is not a valid channel number.
*
* @private util of `@promptbook/color`
*/
function checkChannelValue(channelName, value) {
if (typeof value !== 'number') {
throw new Error(`${channelName} channel value is not number but ${typeof value}`);
}
if (isNaN(value)) {
throw new Error(`${channelName} channel value is NaN`);
}
if (Math.round(value) !== value) {
throw new Error(`${channelName} channel is not whole number, it is ${value}`);
}
if (value < 0) {
throw new Error(`${channelName} channel is lower than 0, it is ${value}`);
}
if (value > 255) {
throw new Error(`${channelName} channel is greater than 255, it is ${value}`);
}
}
/**
* Shared immutable channel storage and serialization helpers for `Color`.
*
* @private base class of Color
*/
class ColorValue {
constructor(red, green, blue, alpha = 255) {
this.red = red;
this.green = green;
this.blue = blue;
this.alpha = alpha;
checkChannelValue('Red', red);
checkChannelValue('Green', green);
checkChannelValue('Blue', blue);
checkChannelValue('Alpha', alpha);
}
/**
* Shortcut for `red` property
* Number from 0 to 255
* @alias red
*/
get r() {
return this.red;
}
/**
* Shortcut for `green` property
* Number from 0 to 255
* @alias green
*/
get g() {
return this.green;
}
/**
* Shortcut for `blue` property
* Number from 0 to 255
* @alias blue
*/
get b() {
return this.blue;
}
/**
* Shortcut for `alpha` property
* Number from 0 (transparent) to 255 (opaque)
* @alias alpha
*/
get a() {
return this.alpha;
}
/**
* Shortcut for `alpha` property
* Number from 0 (transparent) to 255 (opaque)
* @alias alpha
*/
get opacity() {
return this.alpha;
}
/**
* Shortcut for 1-`alpha` property
*/
get transparency() {
return 255 - this.alpha;
}
clone() {
return take(this.createColor(this.red, this.green, this.blue, this.alpha));
}
toString() {
return this.toHex();
}
toHex() {
if (this.alpha === 255) {
return `#${this.red.toString(16).padStart(2, '0')}${this.green.toString(16).padStart(2, '0')}${this.blue
.toString(16)
.padStart(2, '0')}`;
}
else {
return `#${this.red.toString(16).padStart(2, '0')}${this.green.toString(16).padStart(2, '0')}${this.blue
.toString(16)
.padStart(2, '0')}${this.alpha.toString(16).padStart(2, '0')}`;
}
}
toRgb() {
if (this.alpha === 255) {
return `rgb(${this.red}, ${this.green}, ${this.blue})`;
}
else {
return `rgba(${this.red}, ${this.green}, ${this.blue}, ${Math.round((this.alpha / 255) * 100)}%)`;
}
}
toHsl() {
throw new Error(`Getting HSL is not implemented`);
}
}
/**
* 🎨 List of all 140 color names which are supported by CSS
*
* @public exported from `@promptbook/color`
*/
const CSS_COLORS = {
promptbook: '#79EAFD',
transparent: 'rgba(0,0,0,0)',
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
aqua: '#00ffff',
aquamarine: '#7fffd4',
azure: '#f0ffff',
beige: '#f5f5dc',
bisque: '#ffe4c4',
black: '#000000',
blanchedalmond: '#ffebcd',
blue: '#0000ff',
blueviolet: '#8a2be2',
brown: '#a52a2a',
burlywood: '#deb887',
cadetblue: '#5f9ea0',
chartreuse: '#7fff00',
chocolate: '#d2691e',
coral: '#ff7f50',
cornflowerblue: '#6495ed',
cornsilk: '#fff8dc',
crimson: '#dc143c',
cyan: '#00ffff',
darkblue: '#00008b',
darkcyan: '#008b8b',
darkgoldenrod: '#b8860b',
darkgray: '#a9a9a9',
darkgrey: '#a9a9a9',
darkgreen: '#006400',
darkkhaki: '#bdb76b',
darkmagenta: '#8b008b',
darkolivegreen: '#556b2f',
darkorange: '#ff8c00',
darkorchid: '#9932cc',
darkred: '#8b0000',
darksalmon: '#e9967a',
darkseagreen: '#8fbc8f',
darkslateblue: '#483d8b',
darkslategray: '#2f4f4f',
darkslategrey: '#2f4f4f',
darkturquoise: '#00ced1',
darkviolet: '#9400d3',
deeppink: '#ff1493',
deepskyblue: '#00bfff',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1e90ff',
firebrick: '#b22222',
floralwhite: '#fffaf0',
forestgreen: '#228b22',
fuchsia: '#ff00ff',
gainsboro: '#dcdcdc',
ghostwhite: '#f8f8ff',
gold: '#ffd700',
goldenrod: '#daa520',
gray: '#808080',
grey: '#808080',
green: '#008000',
greenyellow: '#adff2f',
honeydew: '#f0fff0',
hotpink: '#ff69b4',
indianred: '#cd5c5c',
indigo: '#4b0082',
ivory: '#fffff0',
khaki: '#f0e68c',
lavender: '#e6e6fa',
lavenderblush: '#fff0f5',
lawngreen: '#7cfc00',
lemonchiffon: '#fffacd',
lightblue: '#add8e6',
lightcoral: '#f08080',
lightcyan: '#e0ffff',
lightgoldenrodyellow: '#fafad2',
lightgray: '#d3d3d3',
lightgrey: '#d3d3d3',
lightgreen: '#90ee90',
lightpink: '#ffb6c1',
lightsalmon: '#ffa07a',
lightseagreen: '#20b2aa',
lightskyblue: '#87cefa',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#b0c4de',
lightyellow: '#ffffe0',
lime: '#00ff00',
limegreen: '#32cd32',
linen: '#faf0e6',
magenta: '#ff00ff',
maroon: '#800000',
mediumaquamarine: '#66cdaa',
mediumblue: '#0000cd',
mediumorchid: '#ba55d3',
mediumpurple: '#9370db',
mediumseagreen: '#3cb371',
mediumslateblue: '#7b68ee',
mediumspringgreen: '#00fa9a',
mediumturquoise: '#48d1cc',
mediumvioletred: '#c71585',
midnightblue: '#191970',
mintcream: '#f5fffa',
mistyrose: '#ffe4e1',
moccasin: '#ffe4b5',
navajowhite: '#ffdead',
navy: '#000080',
oldlace: '#fdf5e6',
olive: '#808000',
olivedrab: '#6b8e23',
orange: '#ffa500',
orangered: '#ff4500',
orchid: '#da70d6',
palegoldenrod: '#eee8aa',
palegreen: '#98fb98',
paleturquoise: '#afeeee',
palevioletred: '#db7093',
papayawhip: '#ffefd5',
peachpuff: '#ffdab9',
peru: '#cd853f',
pink: '#ffc0cb',
plum: '#dda0dd',
powderblue: '#b0e0e6',
purple: '#800080',
rebeccapurple: '#663399',
red: '#ff0000',
rosybrown: '#bc8f8f',
royalblue: '#4169e1',
saddlebrown: '#8b4513',
salmon: '#fa8072',
sandybrown: '#f4a460',
seagreen: '#2e8b57',
seashell: '#fff5ee',
sienna: '#a0522d',
silver: '#c0c0c0',
skyblue: '#87ceeb',
slateblue: '#6a5acd',
slategray: '#708090',
slategrey: '#708090',
snow: '#fffafa',
springgreen: '#00ff7f',
steelblue: '#4682b4',
tan: '#d2b48c',
teal: '#008080',
thistle: '#d8bfd8',
tomato: '#ff6347',
turquoise: '#40e0d0',
violet: '#ee82ee',
wheat: '#f5deb3',
white: '#ffffff',
whitesmoke: '#f5f5f5',
yellow: '#ffff00',
yellowgreen: '#9acd32',
};
// Note: [💞] Ignore a discrepancy between file name and entity name
/**
* Checks if the given value is a valid hex color string
*
* @param value - value to check
* @returns true if the value is a valid hex color string (e.g., `#009edd`, `#fff`, etc.)
*
* @private function of Color
*/
function isHexColorString(value) {
return (typeof value === 'string' && /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(value));
}
/**
* Constant for short hex lengths.
*/
const SHORT_HEX_LENGTHS = new Set([3, 4]);
/**
* Constant for long hex lengths.
*/
const LONG_HEX_LENGTHS = new Set([6, 8]);
/**
* Parses a hex string into RGBA channel values.
*
* @param hex - Hex value such as `#09d`, `009edd`, `#009eddff`.
* @returns RGBA channel values.
*
* @private function of Color
*/
function parseHexColor(hex) {
const sanitized = hex.startsWith('#') ? hex.substring(1) : hex;
const throwInvalidHex = () => {
throw new Error(`Can not parse color from hex string "${hex}"`);
};
if (SHORT_HEX_LENGTHS.has(sanitized.length)) {
return {
red: parseShortHexChannel(sanitized.charAt(0), throwInvalidHex),
green: parseShortHexChannel(sanitized.charAt(1), throwInvalidHex),
blue: parseShortHexChannel(sanitized.charAt(2), throwInvalidHex),
alpha: sanitized.length === 4 ? parseShortHexChannel(sanitized.charAt(3), throwInvalidHex) : 255,
};
}
if (LONG_HEX_LENGTHS.has(sanitized.length)) {
return {
red: parseLongHexChannel(sanitized, 0, throwInvalidHex),
green: parseLongHexChannel(sanitized, 2, throwInvalidHex),
blue: parseLongHexChannel(sanitized, 4, throwInvalidHex),
alpha: sanitized.length === 8 ? parseLongHexChannel(sanitized, 6, throwInvalidHex) : 255,
};
}
return throwInvalidHex();
}
/**
* Parses short hex channel.
*/
function parseShortHexChannel(char, onError) {
if (!char) {
return onError();
}
const parsed = parseInt(char, 16);
if (Number.isNaN(parsed)) {
return onError();
}
return parsed * 16;
}
/**
* Parses long hex channel.
*/
function parseLongHexChannel(hex, start, onError) {
const segment = hex.substr(start, 2);
if (segment.length < 2) {
return onError();
}
const parsed = parseInt(segment, 16);
if (Number.isNaN(parsed)) {
return onError();
}
return parsed;
}
/**
* Pattern matching hsl.
*/
const HSL_REGEX = /^hsl\(\s*([0-9.]+)\s*,\s*([0-9.]+)%\s*,\s*([0-9.]+)%\s*\)$/;
/**
* Parses an HSL string into RGBA channel values.
*
* @param hsl - HSL string such as `hsl(197.1, 100%, 43.3%)`.
* @returns RGBA channel values.
*
* @private function of Color
*/
function parseHslColor(hsl) {
const match = hsl.match(HSL_REGEX);
if (!match) {
throw new Error(`Invalid hsl string format: "${hsl}"`);
}
const hue = parseFloat(match[1]);
const saturation = parseFloat(match[2]) / 100;
const lightness = parseFloat(match[3]) / 100;
const { red, green, blue } = convertHslToRgb(hue, saturation, lightness);
return {
red,
green,
blue,
alpha: 255,
};
}
/**
* Handles convert hsl to Rgb.
*/
function convertHslToRgb(h, s, l) {
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
const m = l - c / 2;
let r1 = 0;
let g1 = 0;
let b1 = 0;
if (h >= 0 && h < 60) {
r1 = c;
g1 = x;
}
else if (h >= 60 && h < 120) {
r1 = x;
g1 = c;
}
else if (h >= 120 && h < 180) {
g1 = c;
b1 = x;
}
else if (h >= 180 && h < 240) {
g1 = x;
b1 = c;
}
else if (h >= 240 && h < 300) {
r1 = x;
b1 = c;
}
else if (h >= 300 && h < 360) {
r1 = c;
b1 = x;
}
return {
red: Math.round((r1 + m) * 255),
green: Math.round((g1 + m) * 255),
blue: Math.round((b1 + m) * 255),
};
}
/**
* Pattern matching RGB.
*/
const RGB_REGEX = /^rgb\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/;
/**
* Pattern matching rgba.
*/
const RGBA_REGEX = /^rgba\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/;
/**
* Parses an RGB string into RGBA channel values.
*
* @param rgb - RGB string such as `rgb(0%, 62%, 86.7%)`.
* @returns RGBA channel values.
*
* @private function of Color
*/
function parseRgbColor(rgb) {
const match = rgb.match(RGB_REGEX);
if (!match) {
throw new Error(`Invalid rgb string format: "${rgb}"`);
}
return {
red: parseChannelValue(match[1]),
green: parseChannelValue(match[2]),
blue: parseChannelValue(match[3]),
alpha: 255,
};
}
/**
* Parses an RGBA string into RGBA channel values.
*
* @param rgba - RGBA string such as `rgba(0, 158, 221, 0.5)`.
* @returns RGBA channel values.
*
* @private function of Color
*/
function parseRgbaColor(rgba) {
const match = rgba.match(RGBA_REGEX);
if (!match) {
throw new Error(`Invalid rgba string format: "${rgba}"`);
}
return {
red: parseChannelValue(match[1]),
green: parseChannelValue(match[2]),
blue: parseChannelValue(match[3]),
alpha: parseAlphaValue(match[4]),
};
}
/**
* Parses channel value.
*/
function parseChannelValue(value) {
if (value.endsWith('%')) {
const percent = parseFloat(value);
return Math.round((percent / 100) * 255);
}
return Math.round(parseFloat(value));
}
/**
* Parses alpha value.
*/
function parseAlphaValue(value) {
if (value.endsWith('%')) {
const percent = parseFloat(value);
return Math.round((percent / 100) * 255);
}
const parsed = parseFloat(value);
if (parsed <= 1) {
return Math.round(parsed * 255);
}
return Math.round(parsed);
}
/**
* Pattern matching hsl regex.
*
* @private function of Color
*/
const HSL_REGEX_PATTERN = /^hsl\(\s*([0-9.]+)\s*,\s*([0-9.]+)%\s*,\s*([0-9.]+)%\s*\)$/;
/**
* Pattern matching RGB regex.
*
* @private function of Color
*/
const RGB_REGEX_PATTERN = /^rgb\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/;
/**
* Pattern matching rgba regex.
*
* @private function of Color
*/
const RGBA_REGEX_PATTERN = /^rgba\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/;
/**
* Parses a supported color string into RGBA channels.
*
* @param color as a string for example `#009edd`, `rgb(0,158,221)`, `rgb(0%,62%,86.7%)`, `hsl(197.1,100%,43.3%)`, `red`, `darkgrey`,...
* @returns RGBA channel values.
*
* @private function of Color
*/
function parseColorString(color) {
const trimmed = color.trim();
const cssColor = CSS_COLORS[trimmed];
if (cssColor) {
return parseColorString(cssColor);
}
else if (isHexColorString(trimmed)) {
return parseHexColor(trimmed);
}
if (HSL_REGEX_PATTERN.test(trimmed)) {
return parseHslColor(trimmed);
}
else if (RGB_REGEX_PATTERN.test(trimmed)) {
return parseRgbColor(trimmed);
}
else if (RGBA_REGEX_PATTERN.test(trimmed)) {
return parseRgbaColor(trimmed);
}
else {
throw new Error(`Can not create a new Color instance from string "${trimmed}".`);
}
}
/**
* Color object represents an RGB color with alpha channel
*
* Note: There is no fromObject/toObject because the most logical way to serialize color is as a hex string (#009edd)
*
* @public exported from `@promptbook/color`
*/
class Color extends ColorValue {
/**
* Creates a new Color instance from miscellaneous formats
* - It can receive Color instance and just return the same instance
* - It can receive color in string format for example `#009edd`, `rgb(0,158,221)`, `rgb(0%,62%,86.7%)`, `hsl(197.1,100%,43.3%)`
*
* Note: This is not including fromImage because detecting color from an image is heavy task which requires async stuff and we cannot safely determine with overloading if return value will be a promise
*
* @param color
* @returns Color object
*/
static from(color, _isSingleValue = false) {
if (color === '') {
throw new Error(`Can not create color from empty string`);
}
else if (color instanceof Color) {
return take(color);
}
else if (Color.isColor(color)) {
return take(color);
}
else if (typeof color === 'string') {
try {
return Color.fromString(color);
}
catch (error) {
// <- Note: Can not use `assertsError(error)` here because it causes circular dependency
if (_isSingleValue) {
throw error;
}
const parts = color.split(/[\s+,;|]/);
if (parts.length > 0) {
return Color.from(parts[0].trim(), true);
}
else {
throw new Error(`Can not create color from given string "${color}"`);
}
}
}
else {
console.error({ color });
throw new Error(`Can not create color from given object`);
}
}
/**
* Creates a new Color instance from miscellaneous formats
* It just does not throw error when it fails, it returns PROMPTBOOK_COLOR instead
*
* @param color
* @returns Color object
*/
static fromSafe(color) {
try {
return Color.from(color);
}
catch (error) {
// <- Note: Can not use `assertsError(error)` here because it causes circular dependency
console.warn(spaceTrim((block) => `
Color.fromSafe error:
${block(error.message)}
Returning default PROMPTBOOK_COLOR.
`));
return Color.fromString('promptbook');
}
}
/**
* Creates a new Color instance from miscellaneous string formats
*
* @param color as a string for example `#009edd`, `rgb(0,158,221)`, `rgb(0%,62%,86.7%)`, `hsl(197.1,100%,43.3%)`, `red`, `darkgrey`,...
* @returns Color object
*/
static fromString(color) {
return Color.fromColorChannels(parseColorString(color));
}
/**
* Gets common color
*
* @param key as a css string like `midnightblue`
* @returns Color object
*/
static get(key) {
if (!CSS_COLORS[key]) {
throw new Error(`"${key}" is not a common css color.`);
}
return Color.fromString(CSS_COLORS[key]);
}
/**
* Creates a new Color instance from average color of given image
*
* @param image as a source for example `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYJh39z8ABJgCe/ZvAS4AAAAASUVORK5CYII=`
* @returns Color object
*/
static async fromImage(image) {
return Color.fromHex(`#009edd`);
}
/**
* Creates a new Color instance from color in hex format
*
* @param color in hex for example `#009edd`, `009edd`, `#555`,...
* @returns Color object
*/
static fromHex(hex) {
return Color.fromColorChannels(parseHexColor(hex));
}
/**
* Creates a new Color instance from color in hsl format
*
* @param color as a hsl for example `hsl(197.1,100%,43.3%)`
* @returns Color object
*/
static fromHsl(hsl) {
return Color.fromColorChannels(parseHslColor(hsl));
}
/**
* Creates a new Color instance from color in rgb format
*
* @param color as a rgb for example `rgb(0,158,221)`, `rgb(0%,62%,86.7%)`
* @returns Color object
*/
static fromRgbString(rgb) {
return Color.fromColorChannels(parseRgbColor(rgb));
}
/**
* Creates a new Color instance from color in rbga format
*
* @param color as a rgba for example `rgba(0,158,221,0.5)`, `rgb(0%,62%,86.7%,50%)`
* @returns Color object
*/
static fromRgbaString(rgba) {
return Color.fromColorChannels(parseRgbaColor(rgba));
}
/**
* Creates a new Color for color channels values
*
* @param red number from 0 to 255
* @param green number from 0 to 255
* @param blue number from 0 to 255
* @param alpha number from 0 (transparent) to 255 (opaque = default)
* @returns Color object
*/
static fromValues(red, green, blue, alpha = 255) {
return Color.fromColorChannels({ red, green, blue, alpha });
}
/**
* Checks if the given value is a valid Color object.
*
* @param {unknown} value - The value to check.
* @return {value is WithTake<Color>} Returns true if the value is a valid Color object, false otherwise.
*/
static isColor(value) {
if (typeof value !== 'object') {
return false;
}
if (value === null) {
return false;
}
if (typeof value.red !== 'number' ||
typeof value.green !== 'number' ||
typeof value.blue !== 'number' ||
typeof value.alpha !== 'number') {
return false;
}
if (typeof value.then !== 'function') {
return false;
}
return true;
}
/**
* Checks if the given value is a valid hex color string
*
* @param value - value to check
* @returns true if the value is a valid hex color string (e.g., `#009edd`, `#fff`, etc.)
*/
static isHexColorString(value) {
return isHexColorString(value);
}
/**
* Creates new Color object
*
* Note: Consider using one of static methods like `from` or `fromString`
*
* @param red number from 0 to 255
* @param green number from 0 to 255
* @param blue number from 0 to 255
* @param alpha number from 0 (transparent) to 255 (opaque)
*/
constructor(red, green, blue, alpha = 255) {
super(red, green, blue, alpha);
}
createColor(red, green, blue, alpha) {
return new Color(red, green, blue, alpha);
}
static fromColorChannels({ red, green, blue, alpha }) {
return take(new Color(red, green, blue, alpha));
}
}
/**
* Makes color transformer which returns a grayscale version of the color
*
* @param amount from 0 to 1
*
* @public exported from `@promptbook/color`
*/
function grayscale(amount) {
return ({ red, green, blue, alpha }) => {
const average = (red + green + blue) / 3;
red = Math.round(average * amount + red * (1 - amount));
green = Math.round(average * amount + green * (1 - amount));
blue = Math.round(average * amount + blue * (1 - amount));
return Color.fromValues(red, green, blue, alpha);
};
}
/**
* Converts HSL values to RGB values
*
* @param hue [0-1]
* @param saturation [0-1]
* @param lightness [0-1]
* @returns [red, green, blue] [0-255]
*
* @private util of `@promptbook/color`
*/
function hslToRgb(hue, saturation, lightness) {
let red;
let green;
let blue;
if (saturation === 0) {
// achromatic
red = lightness;
green = lightness;
blue = lightness;
}
else {
// TODO: Extract to separate function
const hue2rgb = (p, q, t) => {
if (t < 0)
t += 1;
if (t > 1)
t -= 1;
if (t < 1 / 6)
return p + (q - p) * 6 * t;
if (t < 1 / 2)
return q;
if (t < 2 / 3)
return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = lightness < 0.5 ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation;
const p = 2 * lightness - q;
red = hue2rgb(p, q, hue + 1 / 3);
green = hue2rgb(p, q, hue);
blue = hue2rgb(p, q, hue - 1 / 3);
}
return [Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255)];
}
// TODO: Properly name all used internal variables
/**
* Converts RGB values to HSL values
*
* @param red [0-255]
* @param green [0-255]
* @param blue [0-255]
* @returns [hue, saturation, lightness] [0-1]
*
* @private util of `@promptbook/color`
*/
function rgbToHsl(red, green, blue) {
red /= 255;
green /= 255;
blue /= 255;
const max = Math.max(red, green, blue);
const min = Math.min(red, green, blue);
let hue;
let saturation;
const lightness = (max + min) / 2;
if (max === min) {
// achromatic
hue = 0;
saturation = 0;
}
else {
const d = max - min;
saturation = lightness > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case red:
hue = (green - blue) / d + (green < blue ? 6 : 0);
break;
case green:
hue = (blue - red) / d + 2;
break;
case blue:
hue = (red - green) / d + 4;
break;
default:
hue = 0;
}
hue /= 6;
}
return [hue, saturation, lightness];
}
// TODO: Properly name all used internal variables
/**
* Makes color transformer which lighten the given color
*
* @param amount from 0 to 1
*
* @public exported from `@promptbook/color`
*/
function lighten(amount) {
return ({ red, green, blue, alpha }) => {
const [h, s, lInitial] = rgbToHsl(red, green, blue);
let l = lInitial + amount;
l = Math.max(0, Math.min(l, 1)); // Replace lodash clamp with Math.max and Math.min
const [r, g, b] = hslToRgb(h, s, l);
return Color.fromValues(r, g, b, alpha);
};
}
// TODO: Maybe implement by mix+hsl
/**
* Makes color transformer which saturate the given color
*
* @param amount from -1 to 1
*
* @public exported from `@promptbook/color`
*/
function saturate(amount) {
return ({ red, green, blue, alpha }) => {
const [h, sInitial, l] = rgbToHsl(red, green, blue);
let s = sInitial + amount;
s = Math.max(0, Math.min(s, 1));
const [r, g, b] = hslToRgb(h, s, l);
return Color.fromValues(r, g, b, alpha);
};
}
// TODO: Maybe implement by mix+hsl
/**
* Stable root directory used for Promptbook-owned temporary files and caches.
*
* @private internal utility for Promptbook temporary folders
*/
const PROMPTBOOK_TEMPORARY_DIRECTORY = '.promptbook';
/**
* Builds one normalized project-relative path inside Promptbook's dedicated temporary root.
*
* The returned path intentionally uses `/` separators so the same helper can be reused from
* Node.js and edge-safe code without depending on the Node `path` module.
*
* @private internal utility for Promptbook temporary folders
*/
function getPromptbookTemporaryPath(...pathSegments) {
const normalizedPathSegments = pathSegments.flatMap(splitPathSegments).filter(Boolean);
return [PROMPTBOOK_TEMPORARY_DIRECTORY, ...normalizedPathSegments].join('/');
}
/**
* Normalizes one raw path segment into slash-delimited pieces without empty items.
*/
function splitPathSegments(pathSegment) {
return pathSegment
.split(/[\\/]+/u)
.map((segment) => segment.trim())
.filter(Boolean);
}
// 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
/**
* Color of the Promptbook
*
* TODO: [🗽] Unite branding and make single place for it
*
* @public exported from `@promptbook/core`
*/
const PROMPTBOOK_COLOR = Color.fromString('promptbook');
// <- TODO: [🧠][🈵] Using `Color` here increases the package size approx 3kb, maybe remove it
/**
* Colors for syntax highlighting in the `<BookEditor/>`
*
* TODO: [🗽] Unite branding and make single place for it
*
* @public exported from `@promptbook/core`
*/
({
TITLE: Color.fromHex('#244EA8'),
LINE: Color.fromHex('#eeeeee'),
SEPARATOR: Color.fromHex('#cccccc'),
COMMITMENT: Color.fromHex('#DA0F78'),
NOTE_COMMITMENT: Color.fromHex('#8080807e'),
TODO_COMMITMENT_TEXT: Color.fromHex('#000000'),
TODO_COMMITMENT_BACKGROUND: Color.fromHex('#FFEB3B'),
PARAMETER: Color.fromHex('#8e44ad'),
CODE_BLOCK: Color.fromHex('#7700ffff'),
});
// <- TODO: [🧠][🈵] Using `Color` here increases the package size approx 3kb, maybe remove it
/**
* Chat color of the Promptbook (in chat)
*
* TODO: [🗽] Unite branding and make single place for it
*
* @public exported from `@promptbook/core`
*/
PROMPTBOOK_COLOR.then(lighten(0.1)).then(saturate(0.9)).then(grayscale(0.9));
// <- TODO: [🧠][🈵] Using `Color` and `lighten`, `saturate`,... here increases the package size approx 3kb, maybe remove it
/**
* Color of the user (in chat)
*
* TODO: [🗽] Unite branding and make single place for it
*
* @public exported from `@promptbook/core`
*/
Color.fromHex('#1D4ED8');
// <- TODO: [🧠][🈵] Using `Color` here increases the package size approx 3kb, maybe remove it
/**
* When the title is not provided, the default title is used
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_BOOK_TITLE = `🐙 Untitled agent`;
/**
* 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: [🕝] Make also `AGENTS_DIRNAME_ALTERNATIVES`
/**
* Where to store the temporary downloads
*
* Note: When the folder does not exist, it is created recursively
*
* @public exported from `@promptbook/core`
*/
`./${getPromptbookTemporaryPath('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`
*/
`./${getPromptbookTemporaryPath('execution-cache')}`;
/**
* Where to store the scrape cache
*
* Note: When the folder does not exist, it is created recursively
*
* @public exported from `@promptbook/core`
*/
`./${getPromptbookTemporaryPath('scrape-cache')}`;
// <- 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.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.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.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,
NotAllowed,
DatabaseError,
ConflictError,
// 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, isStackAddedToMessage = true) {
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 (isStackAddedToMessage && stack !== undefined && stack !== '') {
message = spacetrim.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;
}
/**
* Creates a loader which imports one module on the first call and reuses the very same module afterwards
*
* Note: [🐌] Heavy third-party dependencies are imported lazily to keep the startup of the Promptbook CLI fast.
* A statically imported dependency is loaded every single time the bundle is loaded, even when the running
* command never touches it. A lazily imported dependency is loaded only when the feature is really used.
*
* @example
* const loadJsdomModule = createLazyModuleLoader(() => import('jsdom'));
* const { JSDOM } = await loadJsdomModule();
*
* @private internal utility of Promptbook
*/
function createLazyModuleLoader(importModule) {
let importedModulePromise = null;
return function loadModule() {
if (importedModulePromise === null) {
importedModulePromise = importModule();
}
return importedModulePromise;
};
}
// Note: [🐌] Do not convert the lazy `import(...)` calls back to static `import` statements, it would bring back the
// slow startup of the `ptbk` CLI utility
/**
* Tests if given string is valid URL.
*
* Note: [🔂] This function is idempotent.
* Note: Dataurl are considered perfectly valid.
* Note: There are few similar functions:
* - `isValidUrl` *(this one)* which tests any URL
* - `isValidAgentUrl` which tests just agent URL
* - `isValidPipelineUrl` which tests just pipeline 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;
}
}
/**
* Loads the Socket.io client (`socket.io-client`) on demand
*
* Note: [🐌] Loaded lazily to keep the startup of the `ptbk` CLI utility fast
*
* @private internal utility of `createRemoteClient`
*/
const loadSocketIoClientModule = createLazyModuleLoader(() => import('socket.io-client'));
/**
* 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.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
`));
}
const { io } = await loadSocketIoClientModule();
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
*
* 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
*
* @see https://github.com/webgptorg/promptbook/discussions/196
*
* @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`
/**
* 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.
*
* 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
*
* @see https://www.npmjs.com/package/promptbook?activeTab=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:
*/
isUsedInPipelineHead: true,
isUsedInPipelineTask: false,
/**
* Description of the BOOK_VERSION command
*/
description: `Which version of the Book language is the .book.md using`,
/**
* Link to documentation
*/
documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/69',
/**
* Example usages of the BOOK_VERSION command
*/
examples: [`BOOK VERSION ${BOOK_LANGUAGE_VERSION}`, `BOOK ${BOOK_LANGUAGE_VERSION}`],
/**
* Parses the BOOK_VERSION command
*/
parse(input) {
const { args } = input;
const bookVersion = args.pop();
if (bookVersion === undefined) {
throw new ParseError(`Version is required`);
}
if (!isValidPromptbookVersion(bookVersion)) {
throw new ParseError(`Invalid Promptbook version "${bookVersion}"`);
}
if (args.length > 0 && !(((args.length === 1 && args[0]) || '').toUpperCase() === 'VERSION')) {
throw new ParseError(`Can not have more than one Promptbook version`);
}
return {
type: 'BOOK_VERSION',
bookVersion: bookVersion,
};
},
/**
* Apply the BOOK_VERSION command to the `pipelineJson`
*
* Note: `$` is used to indicate that this function mutates given `pipelineJson`
*/
$applyToPipelineJson(command, $pipelineJson) {
// TODO: Warn if the version is overridden
$pipelineJson.bookVersion = command.bookVersion;
},
/**
* Converts the BOOK_VERSION command back to string
*
* Note: This is used in `pipelineJsonToString` utility
*/
stringify(command) {
return `---`; // <- TODO: [🛋] Implement
},
/**
* Reads the BOOK_VERSION command from the `PipelineJson`
*
* Note: This is used in `pipelineJsonToString` utility
*/
takeFromPipelineJson(pipelineJson) {
throw new NotYetImplementedError(`[🛋] Not implemented yet`); // <- TODO: [🛋] Implement
},
};
/**
* 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);
}
/**
* Units of text measurement
*
* @see https://github.com/webgptorg/promptbook/discussions/30
*
* @public exported from `@promptbook/core`
*/
const EXPECTATION_UNITS = ['CHARACTERS', 'WORDS', 'SENTENCES', 'LINES', 'PARAGRAPHS', 'PAGES'];
// TODO: [💝] Unite object for expecting amount and format - remove format
/**
* Function parseNumber will parse number from string
*
* Note: [🔂] This function is idempotent.
* Unlike Number.parseInt, Number.parseFloat it will never ever result in NaN
* Note: it also works only with decimal numbers
*
* @returns parsed number
* @throws {ParseError} if the value is not a number
*
* @public exported from `@promptbook/utils`
*/
function parseNumber(value) {
if (value === null || value === undefined) {
return 0;
}
const originalValue = value;
if (typeof value === 'number') {
value = value.toString(); // <- TODO: Maybe more efficient way to do this
}
if (typeof value !== 'string') {
return 0;
}
value = value.trim();
if (value.startsWith('+')) {
return parseNumber(value.substring(1));
}
if (value.startsWith('-')) {
const number = parseNumber(value.substring(1));
if (number === 0) {
return 0; // <- Note: To prevent -0
}
return -number;
}
value = value.replace(/,/g, '.');
value = value.toUpperCase();
if (value === '') {
return 0;
}
if (value === '♾' || value.startsWith('INF')) {
return Infinity;
}
if (value.includes('/')) {
const [numerator_, denominator_] = value.split('/');
const numerator = parseNumber(numerator_);
const denominator = parseNumber(denominator_);
if (denominator === 0) {
throw new ParseError(`Unable to parse number from "${originalValue}" because denominator is zero`);
}
return numerator / denominator;
}
if (/^(NAN|NULL|NONE|UNDEFINED|ZERO|NO.*)$/.test(value)) {
return 0;
}
if (value.includes('E')) {
const [significand, exponent] = value.split('E');
return parseNumber(significand) * 10 ** parseNumber(exponent);
}
if (!/^[0-9.]+$/.test(value) || value.split('.').length > 2) {
throw new ParseError(`Unable to parse number from "${originalValue}"`);
}
const num = parseFloat(value);
if (isNaN(num)) {
throw new ParseError(`Unexpected NaN when parsing number from "${originalValue}"`);
}
return num;
}
// TODO: Maybe use sth. like safe-eval in fraction/calculation case @see https://www.npmjs.com/package/safe-eval
// TODO: [🧠][🌻] Maybe export through `@promptbook/markdown-utils` not `@promptbook/utils`
/**
* import { WrappedError } from '../../errors/WrappedError';
* import { assertsError } from '../../errors/assertsError';
* Parses the expect command
*
* @see `documentationUrl` for more details
*
* @public exported from `@promptbook/editable`
*/
const expectCommandParser = {
/**
* Name of the command
*/
name: 'EXPECT',
/**
* BOILERPLATE command can be used in:
*/
isUsedInPipelineHead: false,
isUsedInPipelineTask: true,
/**
* Description of the FORMAT command
*/
description: spacetrim.spaceTrim(`
Expect command describes the desired output of the task *(after post-processing)*
It can set limits for the maximum/minimum length of the output, measured in characters, words, sentences, paragraphs or some other shape of the output.
`),
/**
* Link to documentation
*/
documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/30',
/**
* Example usages of the FORMAT command
*/
examples: [
'EXPECT MIN 100 Characters',
'EXPECT MAX 10 Words',
'EXPECT EXACTLY 3 Sentences',
'EXPECT EXACTLY 1 Paragraph',
// <- TODO: 'EXPECT 1 Paragraph',
],
/**
* Parses the FORMAT command
*/
parse(input) {
const { args } = input;
try {
let sign;
const signRaw = args.shift();
if (/^exact/i.test(signRaw)) {
sign = 'EXACTLY';
}
else if (/^min/i.test(signRaw)) {
sign = 'MINIMUM';
}
else if (/^max/i.test(signRaw)) {
sign = 'MAXIMUM';
}
else {
throw new ParseError(`Invalid sign "${signRaw}", expected EXACTLY, MIN or MAX`);
}
const amountRaw = args.shift();
const amount = parseNumber(amountRaw);
if (amount < 0) {
throw new ParseError('Amount must be positive number or zero');
}
if (amount !== Math.floor(amount)) {
throw new ParseError('Amount must be whole number');
}
const unitRaw = args.shift();
let unit = undefined;
for (const existingUnit of EXPECTATION_UNITS) {
let existingUnitText = existingUnit;
existingUnitText = existingUnitText.substring(0, existingUnitText.length - 1);
if (existingUnitText === 'CHARACTER') {
existingUnitText = 'CHAR';
}
if (new RegExp(`^${existingUnitText.toLowerCase()}`).test(unitRaw.toLowerCase()) ||
new RegExp(`^${unitRaw.toLowerCase()}`).test(existingUnitText.toLowerCase())) {
if (unit !== undefined) {
throw new ParseError(`Ambiguous unit "${unitRaw}"`);
}
unit = existingUnit;
}
}
if (unit === undefined) {
throw new ParseError(`Invalid unit "${unitRaw}"`);
}
return {
type: 'EXPECT',
sign,
unit,
amount,
};
}
catch (error) {
assertsError(error);
throw new ParseError(spacetrim.spaceTrim((block) => `
Invalid FORMAT command
${block(error.message)}:
`));
}
},
/**
* Apply the FORMAT command to the `pipelineJson`
*
* Note: `$` is used to indicate that this function mutates given `taskJson`
*/
$applyToTaskJson(command, $taskJson) {
// eslint-disable-next-line no-case-declarations
const unit = command.unit.toLowerCase();
$taskJson.expectations = $taskJson.expectations || {};
$taskJson.expectations[unit] = $taskJson.expectations[unit] || {};
if (command.sign === 'MINIMUM' || command.sign === 'EXACTLY') {
if ($taskJson.expectations[unit].min !== undefined) {
throw new ParseError(`Already defined minimum ${$taskJson.expectations[unit].min} ${command.unit.toLowerCase()}, now trying to redefine it to ${command.amount}`);
}
$taskJson.expectations[unit].min = command.amount;
} /* not else */
if (command.sign === 'MAXIMUM' || command.sign === 'EXACTLY') {
if ($taskJson.expectations[unit].max !== undefined) {
throw new ParseError(`Already defined maximum ${$taskJson.expectations[unit].max} ${command.unit.toLowerCase()}, now trying to redefine it to ${command.amount}`);
}
$taskJson.expectations[unit].max = command.amount;
}
},
/**
* Converts the FORMAT command back to string
*
* Note: This is used in `pipelineJsonToString` utility
*/
stringify(command) {
return `---`; // <- TODO: [🛋] Implement
},
/**
* Reads the FORMAT command from the `TaskJson`
*
* Note: This is used in `pipelineJsonToString` utility
*/
takeFromTaskJson($taskJson) {
throw new NotYetImplementedError(`[🛋] Not implemented yet`); // <- TODO: [🛋] Implement
},
};
/**
* Contains configuration options for parsing and generating CSV files, such as delimiters and quoting rules.
*
* @public exported from `@promptbook/core`
*/
const MANDATORY_CSV_SETTINGS = Object.freeze({
header: true,
// encoding: 'utf-8',
});
/**
* Converts a CSV string into an object
*
* Note: This is wrapper around `papaparse.parse()` with better autohealing
*
* @private - for now until `@promptbook/csv` is released
*/
function csvParse(value /* <- TODO: string_csv */, settings, schema /* <- TODO: Make CSV Schemas */) {
settings = { ...settings, ...MANDATORY_CSV_SETTINGS };
// Note: Autoheal invalid '\n' characters
if (settings.newline && !settings.newline.includes('\r') && value.includes('\r')) {
console.warn('CSV string contains carriage return characters, but in the CSV settings the `newline` setting does not include them. Autohealing the CSV string.');
value = value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
}
const csv = papaparse__default["default"].parse(value, settings);
return csv;
}
/**
* Function to check if a string is valid CSV
*
* @param value The string to check
* @returns `true` if the string is a valid CSV string, false otherwise
*
* @public exported from `@promptbook/utils`
*/
function isValidCsvString(value) {
try {
// A simple check for CSV format: at least one comma and no invalid characters
if (value.includes(',') && /^[\w\s,"']+$/.test(value)) {
return true;
}
return false;
}
catch (error) {
assertsError(error);
return false;
}
}
/**
* Definition for CSV spreadsheet
*
* TODO: [🏢] Export from package `@promptbook/csv`
*
* @public exported from `@promptbook/core`
*/
const CsvFormatParser = {
formatName: 'CSV',
aliases: ['SPREADSHEET', 'TABLE'],
isValid(value, settings, schema) {
return isValidCsvString(value);
},
canBeValid(partialValue, settings, schema) {
return true;
},
heal(value, settings, schema) {
throw new Error('Not implemented');
},
subvalueParsers: [
{
subvalueName: 'ROW',
async mapValues(options) {
const { value, outputParameterName, settings, mapCallback, onProgress } = options;
const csv = csvParse(value, settings);
if (csv.errors.length !== 0) {
throw new CsvFormatError(spacetrim.spaceTrim((block) => `
CSV parsing error
Error(s) from CSV parsing:
${block(csv.errors.map((error) => error.message).join('\n\n'))}
The CSV setings:
${block(JSON.stringify({ ...settings, ...MANDATORY_CSV_SETTINGS }, null, 2))}
The CSV data:
${block(value)}
`));
}
const mappedData = [];
const length = csv.data.length;
for (let index = 0; index < length; index++) {
const row = csv.data[index];
if (row[outputParameterName]) {
throw new CsvFormatError(`Can not overwrite existing column "${outputParameterName}" in CSV row`);
}
const mappedRow = {
...row,
[outputParameterName]: await mapCallback(row, index, length),
};
mappedData.push(mappedRow);
if (onProgress) {
// Note: Report the CSV with all rows mapped so far
/*
// TODO: [🛕] Report progress with all the rows including the pending ones
const progressData = mappedData.map((row, i) =>
i > index ? { ...row, [outputParameterName]: PENDING_VALUE_PLACEHOLDER } : row,
);
*/
await onProgress(papaparse__default["default"].unparse(mappedData, { ...settings, ...MANDATORY_CSV_SETTINGS }));
}
}
return papaparse__default["default"].unparse(mappedData, { ...settings, ...MANDATORY_CSV_SETTINGS });
},
},
{
subvalueName: 'CELL',
async mapValues(options) {
const { value, settings, mapCallback, onProgress } = options;
const csv = csvParse(value, settings);
if (csv.errors.length !== 0) {
throw new CsvFormatError(spacetrim.spaceTrim((block) => `
CSV parsing error
Error(s) from CSV parsing:
${block(csv.errors.map((error) => error.message).join('\n\n'))}
The CSV setings:
${block(JSON.stringify({ ...settings, ...MANDATORY_CSV_SETTINGS }, null, 2))}
The CSV data:
${block(value)}
`));
}
const mappedData = await Promise.all(csv.data.map(async (row, rowIndex) => {
return /* not await */ Promise.all(Object.entries(row).map(async ([key, value], columnIndex, array) => {
const index = rowIndex * Object.keys(row).length + columnIndex;
return /* not await */ mapCallback({ [key]: value }, index, array.length);
}));
}));
return papaparse__default["default"].unparse(mappedData, { ...settings, ...MANDATORY_CSV_SETTINGS });
},
},
],
};
// TODO: [🍓] In `CsvFormatParser` implement simple `isValid`
// TODO: [🍓] In `CsvFormatParser` implement partial `canBeValid`
// TODO: [🍓] In `CsvFormatParser` implement `heal
// TODO: [🍓] In `CsvFormatParser` implement `subvalueParsers`
// TODO: [🏢] Allow to expect something inside CSV objects and other formats
/**
* Function isValidJsonString will tell you if the string is valid JSON or not
*
* @param value The string to check
* @returns `true` if the string is a valid JSON string, false otherwise
*
* @public exported from `@promptbook/utils`
*/
function isValidJsonString(value /* <- [👨⚖️] */) {
try {
JSON.parse(value);
return true;
}
catch (error) {
assertsError(error);
if (error.message.includes('Unexpected token')) {
return false;
}
return false;
}
}
/**
* Definition for JSON format
*
* @private still in development [🏢]
*/
const JsonFormatParser = {
formatName: 'JSON',
mimeType: 'application/json',
isValid(value, settings, schema) {
return isValidJsonString(value);
},
canBeValid(partialValue, settings, schema) {
return true;
},
heal(value, settings, schema) {
throw new Error('Not implemented');
},
subvalueParsers: [],
};
// TODO: [🧠] Maybe proper instance of object
// TODO: [0] Make string_serialized_json
// TODO: [1] Make type for JSON Settings and Schema
// TODO: [🧠] What to use for validating JSONs - JSON Schema, ZoD, typescript types/interfaces,...?
// TODO: [🍓] In `JsonFormatParser` implement simple `isValid`
// TODO: [🍓] In `JsonFormatParser` implement partial `canBeValid`
// TODO: [🍓] In `JsonFormatParser` implement `heal
// TODO: [🍓] In `JsonFormatParser` implement `subvalueParsers`
// TODO: [🏢] Allow to expect something inside JSON objects and other formats
/**
* Definition for any text - this will be always valid
*
* Note: This is not useful for validation, but for splitting and mapping with `subvalueParsers`
*
* @public exported from `@promptbook/core`
*/
const TextFormatParser = {
formatName: 'TEXT',
isValid(value) {
return typeof value === 'string';
},
canBeValid(partialValue) {
return typeof partialValue === 'string';
},
heal() {
throw new UnexpectedError('It does not make sense to call `TextFormatParser.heal`');
},
subvalueParsers: [
{
subvalueName: 'LINE',
async mapValues(options) {
const { value, mapCallback, onProgress } = options;
const lines = value.split(/\r?\n/);
const mappedLines = await Promise.all(lines.map((lineContent, lineNumber, array) =>
// TODO: [🧠] Maybe option to skip empty line
/* not await */ mapCallback({
lineContent,
// TODO: [🧠] Maybe also put here `lineNumber`
}, lineNumber, array.length)));
return mappedLines.join('\n');
},
},
// <- TODO: [🧠][🤠] Here should be all words, characters, lines, paragraphs, pages available as subvalues
],
};
// TODO: [1] Make type for XML Text and Schema
// TODO: [🧠][🤠] Here should be all words, characters, lines, paragraphs, pages available as subvalues
// TODO: [🍓] In `TextFormatParser` implement simple `isValid`
// TODO: [🍓] In `TextFormatParser` implement partial `canBeValid`
// TODO: [🍓] In `TextFormatParser` implement `heal
// TODO: [🍓] In `TextFormatParser` implement `subvalueParsers`
// TODO: [🏢] Allow to expect something inside each item of list and other formats
/**
* Function to check if a string is valid XML
*
* @param value
* @returns `true` if the string is a valid XML string, false otherwise
*
* @public exported from `@promptbook/utils`
*/
function isValidXmlString(value) {
try {
const parser = new DOMParser();
const parsedDocument = parser.parseFromString(value, 'application/xml');
const parserError = parsedDocument.getElementsByTagName('parsererror');
if (parserError.length > 0) {
return false;
}
return true;
}
catch (error) {
assertsError(error);
return false;
}
}
/**
* Definition for XML format
*
* @private still in development [🏢]
*/
const XmlFormatParser = {
formatName: 'XML',
mimeType: 'application/xml',
isValid(value, settings, schema) {
return isValidXmlString(value);
},
canBeValid(partialValue, settings, schema) {
return true;
},
heal(value, settings, schema) {
throw new Error('Not implemented');
},
subvalueParsers: [],
};
// TODO: [🧠] Maybe proper instance of object
// TODO: [0] Make string_serialized_xml
// TODO: [1] Make type for XML Settings and Schema
// TODO: [🧠] What to use for validating XMLs - XSD,...
// TODO: [🍓] In `XmlFormatParser` implement simple `isValid`
// TODO: [🍓] In `XmlFormatParser` implement partial `canBeValid`
// TODO: [🍓] In `XmlFormatParser` implement `heal
// TODO: [🍓] In `XmlFormatParser` implement `subvalueParsers`
// TODO: [🏢] Allow to expect something inside XML and other formats
/**
* Definitions for all formats supported by Promptbook
*
* @private internal index of `...` <- TODO [🏢]
*/
const FORMAT_DEFINITIONS = [JsonFormatParser, XmlFormatParser, TextFormatParser, CsvFormatParser];
// Note: [💞] Ignore a discrepancy between file name and entity name
/**
* Normalizes a text string to SCREAMING_CASE (all uppercase with underscores).
*
* Note: [🔂] This function is idempotent.
*
* @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
*/
/**
* Orders JSON object by keys
*
* @returns The same type of object as the input re-ordered
*
* @public exported from `@promptbook/utils`
*/
function orderJson(options) {
const { value, order } = options;
const orderedValue = {
...(order === undefined ? {} : Object.fromEntries(order.map((key) => [key, undefined]))),
...value,
};
return orderedValue;
}
/**
* Freezes the given object and all its nested objects recursively
*
* Note: `$` is used to indicate that this function is not a pure function - it mutates given object
* Note: This function mutates the object and returns the original (but mutated-deep-freezed) object
*
* @returns The same object as the input, but deeply frozen
*
* @public exported from `@promptbook/utils`
*/
function $deepFreeze(objectValue) {
if (Array.isArray(objectValue)) {
return Object.freeze(objectValue.map((item) => $deepFreeze(item)));
}
const propertyNames = Object.getOwnPropertyNames(objectValue);
for (const propertyName of propertyNames) {
const value = objectValue[propertyName];
if (value && typeof value === 'object') {
$deepFreeze(value);
}
}
Object.freeze(objectValue);
return objectValue;
}
// TODO: [🧠] Is there a way how to meaningfully test this utility
/**
* Checks if the value is [🚉] serializable as JSON
* If not, throws an UnexpectedError with a rich error message and tracking
*
* - Almost all primitives are serializable BUT:
* - `undefined` is not serializable
* - `NaN` is not serializable
* - Objects and arrays are serializable if all their properties are serializable
* - Functions are not serializable
* - Circular references are not serializable
* - `Date` objects are not serializable
* - `Map` and `Set` objects are not serializable
* - `RegExp` objects are not serializable
* - `Error` objects are not serializable
* - `Symbol` objects are not serializable
* - And much more...
*
* @throws UnexpectedError if the value is not serializable as JSON
*
* @public exported from `@promptbook/utils`
*/
function checkSerializableAsJson(options) {
checkSerializableValue(options);
}
// 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
/**
* Checks one value and dispatches to the appropriate specialized validator.
*
* @private function of `checkSerializableAsJson`
*/
function checkSerializableValue(options) {
const { value } = options;
if (isSerializablePrimitive(value)) {
return;
}
if (value === undefined) {
throw new UnexpectedError(`${options.name} is undefined`);
}
if (typeof value === 'symbol') {
throw new UnexpectedError(`${options.name} is symbol`);
}
if (typeof value === 'function') {
throw new UnexpectedError(`${options.name} is function`);
}
if (Array.isArray(value)) {
checkSerializableArray(options, value);
return;
}
if (value !== null && typeof value === 'object') {
checkSerializableObject(options, value);
return;
}
throwUnknownTypeError(options);
}
/**
* Checks the primitive values that are directly JSON serializable.
*
* @private function of `checkSerializableAsJson`
*/
function isSerializablePrimitive(value) {
return (value === null ||
typeof value === 'boolean' ||
(typeof value === 'number' && !isNaN(value)) ||
typeof value === 'string');
}
/**
* Recursively checks JSON array items.
*
* @private function of `checkSerializableAsJson`
*/
function checkSerializableArray(context, arrayValue) {
for (let index = 0; index < arrayValue.length; index++) {
checkSerializableAsJson({
...context,
name: `${context.name}[${index}]`,
value: arrayValue[index],
});
}
}
/**
* Checks object-like values and dispatches special unsupported built-ins.
*
* @private function of `checkSerializableAsJson`
*/
function checkSerializableObject(context, objectValue) {
checkUnsupportedObjectType(context, objectValue);
checkSerializableObjectEntries(context, objectValue);
assertJsonStringificationSucceeds(context, objectValue);
}
/**
* Rejects built-in objects that must be converted before JSON serialization.
*
* @private function of `checkSerializableAsJson`
*/
function checkUnsupportedObjectType(context, objectValue) {
if (objectValue instanceof Date) {
throw new UnexpectedError(spacetrim.spaceTrim((block) => `
\`${context.name}\` is Date
Use \`string_date_iso8601\` instead
Additional message for \`${context.name}\`:
${block(context.message || '(nothing)')}
`));
}
if (objectValue instanceof Map) {
throw new UnexpectedError(`${context.name} is Map`);
}
if (objectValue instanceof Set) {
throw new UnexpectedError(`${context.name} is Set`);
}
if (objectValue instanceof RegExp) {
throw new UnexpectedError(`${context.name} is RegExp`);
}
if (objectValue instanceof Error) {
throw new UnexpectedError(spacetrim.spaceTrim((block) => `
\`${context.name}\` is unserialized Error
Use function \`serializeError\`
Additional message for \`${context.name}\`:
${block(context.message || '(nothing)')}
`));
}
}
/**
* Recursively checks object properties while preserving omitted `undefined` keys.
*
* @private function of `checkSerializableAsJson`
*/
function checkSerializableObjectEntries(context, objectValue) {
for (const [subName, subValue] of Object.entries(objectValue)) {
if (subValue === undefined) {
// Note: undefined in object is serializable - it is just omitted
continue;
}
checkSerializableAsJson({
...context,
name: `${context.name}.${subName}`,
value: subValue,
});
}
}
/**
* Uses `JSON.stringify` as the final guard for cases like circular references.
*
* @private function of `checkSerializableAsJson`
*/
function assertJsonStringificationSucceeds(context, objectValue) {
try {
JSON.stringify(objectValue); // <- TODO: [0]
}
catch (error) {
assertsError(error);
throw new UnexpectedError(spacetrim.spaceTrim((block) => `
\`${context.name}\` is not serializable
${block(error.stack || error.message)}
Additional message for \`${context.name}\`:
${block(context.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 })));
}
}
}
*/
}
/**
* Throws the fallback error for unsupported value types like `bigint` and `NaN`.
*
* @private function of `checkSerializableAsJson`
*/
function throwUnknownTypeError(context) {
throw new UnexpectedError(spacetrim.spaceTrim((block) => `
\`${context.name}\` is unknown type
Additional message for \`${context.name}\`:
${block(context.message || '(nothing)')}
`));
}
/**
* Creates a deep clone of the given object
*
* Note: [🔂] This function is idempotent.
* 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 chococake)[propertyName];
> if (value && typeof value === 'object') {
> deepClone(value);
> }
> }
> return Object.assign({}, objectValue);
*/
}
// TODO: [🧠] Is there a way how to meaningfully test this utility
/**
* Utility to export a JSON object from a function
*
* 1) Checks if the value is serializable as JSON
* 2) Makes a deep clone of the object
* 2) Orders the object properties
* 2) Deeply freezes the cloned object
*
* Note: This function does not mutates the given object
*
* @returns The same type of object as the input but read-only and re-ordered
*
* @public exported from `@promptbook/utils`
*/
function exportJson(options) {
const { name, value, order, message } = options;
checkSerializableAsJson({ name, value, message });
const orderedValue =
// TODO: Fix error "Type instantiation is excessively deep and possibly infinite."
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
order === undefined
? deepClone(value)
: orderJson({
value: value,
// <- Note: checkSerializableAsJson asserts that the value is serializable as JSON
order: order,
});
$deepFreeze(orderedValue);
return orderedValue;
}
// TODO: [🧠] Is there a way how to meaningfully test this utility
/**
* Order of keys in the pipeline JSON
*
* @public exported from `@promptbook/core`
*/
const ORDER_OF_PIPELINE_JSON = [
// Note: [🍙] In this order will be pipeline serialized
'title',
'pipelineUrl',
'bookVersion',
'description',
'formfactorName',
'parameters',
'tasks',
'personas',
'preparations',
'knowledgeSources',
'knowledgePieces',
'sources', // <- TODO: [🧠] Where should the `sources` be
];
/**
* 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
/**
* Normalizes a given text to camelCase format.
*
* Note: [🔂] This function is idempotent.
*
* @param text The text to be normalized.
* @param _isFirstLetterCapital Whether the first letter should be capitalized.
* @returns The camelCase formatted string.
* @example 'helloWorld'
* @example 'iLovePromptbook'
*
* @public exported from `@promptbook/utils`
*/
function normalizeTo_camelCase(text, _isFirstLetterCapital = false) {
let charType;
let lastCharType = null;
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 (!lastCharType) {
if (_isFirstLetterCapital) {
normalizedChar = normalizedChar.toUpperCase(); //TODO: DRY
}
}
else if (charType !== lastCharType &&
!(charType === 'LOWERCASE' && lastCharType === 'UPPERCASE') &&
!(lastCharType === 'NUMBER') &&
!(charType === 'NUMBER')) {
normalizedChar = normalizedChar.toUpperCase(); //TODO: [🌺] DRY
}
normalizedName += normalizedChar;
lastCharType = charType;
}
return normalizedName;
}
// TODO: [🌺] Use some intermediate util splitWords
/**
* Collection of default diacritics removal map.
*/
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, (character) => {
return DIACRITIC_VARIANTS_LETTERS[character] || character;
});
}
// TODO: [Ж] Variant for cyrillic (and in general non-latin) letters
/**
* Removes emojis from a string and fix whitespaces
*
* Note: [🔂] This function is idempotent.
*
* @param text with emojis
* @returns text without emojis
*
* @public exported from `@promptbook/utils`
*/
function removeEmojis(text) {
// Replace emojis (and also ZWJ sequence) with hyphens
text = text.replace(/(\p{Extended_Pictographic})\p{Modifier_Symbol}/gu, '$1');
text = text.replace(/(\p{Extended_Pictographic})[\u{FE00}-\u{FE0F}]/gu, '$1');
text = text.replace(/(\p{Extended_Pictographic})(\u{200D}\p{Extended_Pictographic})*/gu, '$1');
text = text.replace(/\p{Extended_Pictographic}/gu, '');
return text;
}
/**
* Removes quotes from a string
*
* Note: [🔂] This function is idempotent.
* Tip: This is very useful for post-processing of the result of the LLM model
* Note: This function removes only the same quotes from the beginning and the end of the string
* Note: There are two similar functions:
* - `removeQuotes` which removes only bounding quotes
* - `unwrapResult` which removes whole introduce sentence
*
* @param text optionally quoted text
* @returns text without quotes
*
* @public exported from `@promptbook/utils`
*/
function removeQuotes(text) {
if (text.startsWith('"') && text.endsWith('"')) {
return text.slice(1, -1);
}
if (text.startsWith("'") && text.endsWith("'")) {
return text.slice(1, -1);
}
return text;
}
/**
* Function `validateParameterName` will normalize and validate a parameter name for use in pipelines.
* It removes diacritics, emojis, and quotes, normalizes to camelCase, and checks for reserved names and invalid characters.
*
* Note: [🔂] This function is idempotent.
*
* @param parameterName The parameter name to validate and normalize.
* @returns The validated and normalized parameter name.
* @throws {ParseError} If the parameter name is empty, reserved, or contains invalid characters.
*
* @private within the repository
*/
function validateParameterName(parameterName) {
const rawParameterName = parameterName;
for (const [start, end] of [
['`', '`'],
['{', '}'],
['[', ']'],
['(', ')'],
['<', '>'],
]) {
if (parameterName.substring(0, 1) === start &&
parameterName.substring(parameterName.length - 1, parameterName.length) === end
// <- TODO: More universal that 1 character
) {
parameterName = parameterName.substring(1, parameterName.length - 1);
// <- TODO: More universal that 1 character
}
}
// TODO: [🐠] Following try-catch block should be part of common validators logic
try {
/*
Note: We don't need to check for spaces because we are going to normalize the parameter name to camelCase
if (parameterName.includes(' ')) {
throw new ParseError(`Parameter name cannot contain spaces`);
}
*/
if (parameterName.includes('.')) {
throw new ParseError(`Parameter name cannot contain dots`);
}
if (parameterName.includes('/') || parameterName.includes('\\')) {
throw new ParseError(`Parameter name cannot contain slashes`);
}
if (parameterName.includes('(') ||
parameterName.includes(')') ||
parameterName.includes('{') ||
parameterName.includes('}') ||
parameterName.includes('[') ||
parameterName.includes(']')) {
throw new ParseError(`Parameter name cannot contain braces`);
}
parameterName = removeDiacritics(parameterName);
parameterName = removeEmojis(parameterName);
parameterName = removeQuotes(parameterName);
parameterName = normalizeTo_camelCase(parameterName);
if (parameterName === '') {
throw new ParseError(`Parameter name cannot be empty`);
}
if (RESERVED_PARAMETER_NAMES.includes(parameterName)) {
throw new ParseError(`{${parameterName}} is a reserved parameter name`);
}
}
catch (error) {
if (!(error instanceof ParseError)) {
throw error;
}
throw new ParseError(spacetrim.spaceTrim((block) => `
${block(error.message)}
Tried to validate parameter name:
${block(rawParameterName)}
`));
}
return parameterName;
}
/**
* Parses the foreach command
*
* @see `documentationUrl` for more details
*
* @public exported from `@promptbook/editable`
*/
const foreachCommandParser = {
/**
* Name of the command
*/
name: 'FOREACH',
/**
* Aliases for the FOREACH command
*/
aliasNames: ['FOR', 'EACH'],
/**
* FOREACH command can be used in:
*/
isUsedInPipelineHead: false,
isUsedInPipelineTask: true,
/**
* Description of the FOREACH command
*/
description: `@@`,
/**
* Link to documentation
*/
documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/148',
/**
* Example usages of the FOREACH command
*/
examples: [
'FOREACH Text Line `{customers}` -> `{customer}`',
'FOREACH Csv Cell `{customers}` -> `{cell}`',
'FOREACH Csv Row `{customers}` -> `{firstName}`, `{lastName}`, `+{email}`',
'FOR Text Line `{customers}` -> `{customer}`',
'EACH Text Line `{customers}` -> `{customer}`',
],
/**
* Parses the FOREACH command
*/
parse(input) {
const { args } = input;
const formatName = normalizeTo_SCREAMING_CASE(args[0] || '');
const subformatName = normalizeTo_SCREAMING_CASE(args[1] || '');
const parameterNameArg = args[2] || '';
const assignSign = args[3];
const formatDefinition = FORMAT_DEFINITIONS.find((formatDefinition) => [formatDefinition.formatName, ...(formatDefinition.aliases || [])].includes(formatName));
if (formatDefinition === undefined) {
throw new ParseError(spacetrim.spaceTrim((block) => `
Unsupported format "${formatName}"
Available formats:
${block(FORMAT_DEFINITIONS.map((formatDefinition) => formatDefinition.formatName)
.map((formatName) => `- ${formatName}`)
.join('\n'))}
`));
// <- TODO: [🏢] List all supported format names
}
const subvalueParser = formatDefinition.subvalueParsers.find((subvalueParser) => [subvalueParser.subvalueName, ...(subvalueParser.aliases || [])].includes(subformatName));
if (subvalueParser === undefined) {
throw new ParseError(spacetrim.spaceTrim((block) => `
Unsupported subformat name "${subformatName}" for format "${formatName}"
Available subformat names for format "${formatDefinition.formatName}":
${block(formatDefinition.subvalueParsers
.map((subvalueParser) => subvalueParser.subvalueName)
.map((subvalueName) => `- ${subvalueName}`)
.join('\n'))}
`));
// <- TODO: [🏢] List all supported subformat names for the format
}
if (assignSign !== '->') {
throw new ParseError(`FOREACH command must have '->' to assign the value to the parameter`);
}
const parameterName = validateParameterName(parameterNameArg);
let outputSubparameterName = null;
// TODO: [4] DRY
const inputSubparameterNames = args
.slice(4)
.map((parameterName) => parameterName.split(',').join(' ').trim())
.filter((parameterName) => !parameterName.includes('+'))
.filter((parameterName) => parameterName !== '')
.map(validateParameterName);
// TODO: [4] DRY
const outputSubparameterNames = args
.slice(4)
.map((parameterName) => parameterName.split(',').join(' ').trim())
.filter((parameterName) => parameterName.includes('+'))
.map((parameterName) => parameterName.split('+').join(''))
.map(validateParameterName);
if (outputSubparameterNames.length === 1) {
outputSubparameterName = outputSubparameterNames[0];
}
else if (outputSubparameterNames.length > 1) {
throw new ParseError(`FOREACH command can not have more than one output subparameter`);
}
if (inputSubparameterNames.length === 0) {
throw new ParseError(`FOREACH command must have at least one input subparameter`);
}
if (outputSubparameterName === null) {
// TODO: Following code should be unhardcoded from here and moved to the format definition
if (formatName === 'CSV' && subformatName === 'CELL') {
outputSubparameterName = 'newCell';
}
else if (formatName === 'TEXT' && subformatName === 'LINE') {
outputSubparameterName = 'newLine';
}
else {
throw new ParseError(spacetrim.spaceTrim(`
FOREACH ${formatName} ${subformatName} must specify output subparameter
Correct example:
- FOREACH ${formatName} ${subformatName} {${parameterName}} -> {inputSubparameterName1}, {inputSubparameterName2}, +{outputSubparameterName}
`));
}
}
return {
type: 'FOREACH',
formatName,
subformatName,
parameterName,
inputSubparameterNames,
outputSubparameterName,
};
},
/**
* Apply the FOREACH command to the `pipelineJson`
*
* Note: `$` is used to indicate that this function mutates given `taskJson`
*/
$applyToTaskJson(command, $taskJson, $pipelineJson) {
const { formatName, subformatName, parameterName, inputSubparameterNames, outputSubparameterName } = command;
// TODO: [🍭] Detect double use
// TODO: [🍭] Detect usage with JOKER and don't allow it
$taskJson.foreach = {
formatName,
subformatName,
parameterName,
inputSubparameterNames,
outputSubparameterName,
};
// Note: [🍭] FOREACH apply has some sideeffects on different places in codebase
},
/**
* Converts the FOREACH command back to string
*
* Note: This is used in `pipelineJsonToString` utility
*/
stringify(command) {
return `---`; // <- TODO: [🛋] Implement
},
/**
* Reads the FOREACH command from the `TaskJson`
*
* Note: This is used in `pipelineJsonToString` utility
*/
takeFromTaskJson($taskJson) {
throw new NotYetImplementedError(`[🛋] Not implemented yet`); // <- TODO: [🛋] Implement
},
};
// TODO: [🍭] Make .book.md file with examples of the FOREACH with wrong parsing and logic
/**
* Parses the format command
*
* @see `documentationUrl` for more details
*
* @public exported from `@promptbook/editable`
*/
const formatCommandParser = {
/**
* Name of the command
*/
name: 'FORMAT',
/**
* BOILERPLATE command can be used in:
*/
isUsedInPipelineHead: false,
isUsedInPipelineTask: true,
/**
* Description of the FORMAT command
*/
description: spacetrim.spaceTrim(`
Format command describes the desired output of the task (after post-processing)
It can set limits for the maximum/minimum length of the output, measured in characters, words, sentences, paragraphs or some other shape of the output.
`),
/**
* Link to documentation
*/
documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/30',
/**
* Example usages of the FORMAT command
*/
examples: ['FORMAT JSON'],
/**
* Parses the FORMAT command
*/
parse(input) {
const { normalized } = input;
if (!normalized.startsWith('FORMAT_JSON')) {
throw new ParseError(`For now only JSON format is supported, in future we will support more formats`);
}
return {
type: 'FORMAT',
format: 'JSON',
};
// <- TODO: [🦽] Why this is constantly removed by repair-imports.ts
// [🥤]
},
/**
* Apply the FORMAT command to the `pipelineJson`
*
* Note: `$` is used to indicate that this function mutates given `taskJson`
*/
$applyToTaskJson(command, $taskJson) {
if ($taskJson.format !== undefined && command.format !== $taskJson.format) {
throw new ParseError(spacetrim.spaceTrim(`Format format is already defined to "${$taskJson.format}".
Now you try to redefine it by "${command.format}"`));
}
$taskJson.format = command.format;
},
/**
* Converts the FORMAT command back to string
*
* Note: This is used in `pipelineJsonToString` utility
*/
stringify(command) {
return `---`; // <- TODO: [🛋] Implement
},
/**
* Reads the FORMAT command from the `TaskJson`
*
* Note: This is used in `pipelineJsonToString` utility
*/
takeFromTaskJson($taskJson) {
throw new NotYetImplementedError(`[🛋] Not implemented yet`); // <- TODO: [🛋] Implement
},
};
/**
* Chatbot form factor definition for conversational interfaces that interact with users in a chat-like manner.
*
* @public exported from `@promptbook/core`
*/
const ChatbotFormfactorDefinition = {
name: 'CHATBOT',
aliasNames: ['CHAT'],
description: `A chatbot form factor for conversational user interfaces.`,
documentationUrl: `https://github.com/webgptorg/promptbook/discussions/174`,
pipelineInterface: {
inputParameters: [
{
name: 'previousTitle',
description: `Previous title of the conversation`,
isInput: true,
isOutput: false,
},
{
name: 'previousConversationSummary',
description: `Previous conversation summary`,
isInput: true,
isOutput: false,
},
{ name: 'userMessage', description: `User message`, isInput: true, isOutput: false },
],
outputParameters: [
{ name: 'title', description: `Title of the conversation`, isInput: false, isOutput: true },
{ name: 'conversationSummary', description: `Summary of the conversation`, isInput: false, isOutput: true },
{ name: 'chatbotResponse', description: `Chatbot response`, isInput: false, isOutput: true },
],
},
};
/**
* Completion is formfactor that emulates completion models
*
* @public exported from `@promptbook/core`
*/
const CompletionFormfactorDefinition = {
name: 'COMPLETION',
description: `Completion is formfactor that emulates completion models`,
documentationUrl: `https://github.com/webgptorg/promptbook/discussions/@@`,
// <- TODO: https://github.com/webgptorg/promptbook/discussions/new?category=concepts
// "🔠 Completion Formfactor"
pipelineInterface: {
inputParameters: [
{
name: 'inputText',
description: `Input text to be completed`,
isInput: true,
isOutput: false,
},
{
name: 'instructions',
description: `Additional instructions for the model, for example the required length, empty by default`,
isInput: true,
isOutput: false,
},
],
outputParameters: [
{
name: 'followingText',
description: `Text that follows the input text`,
isInput: false,
isOutput: true,
},
],
},
};
/**
* Generator form factor represents an application that generates content or data based on user input or predefined rules.
* This form factor is used for apps that produce outputs, such as text, images, or other media, based on provided input.
*
* @public exported from `@promptbook/core`
*/
const GeneratorFormfactorDefinition = {
name: 'GENERATOR',
description: `Generates any kind (in HTML with possible scripts and css format) of content from input message`,
documentationUrl: `https://github.com/webgptorg/promptbook/discussions/184`,
pipelineInterface: {
inputParameters: [
{
name: 'inputMessage',
description: `Input message to be image made from`,
isInput: true,
isOutput: false,
},
],
outputParameters: [
{
name: 'result',
description: `Result in HTML to be shown to user`,
isInput: false,
isOutput: true,
},
],
},
};
/**
* Pipeline interface which is equivalent to `any`
*
* @see https://github.com/webgptorg/promptbook/discussions/171
*
* @public exported from `@promptbook/core`
*/
const GENERIC_PIPELINE_INTERFACE = {
inputParameters: [],
outputParameters: [],
};
// Note: [💞] Ignore a discrepancy between file name and entity name
/**
* A generic pipeline
*
* @public exported from `@promptbook/core`
*/
const GenericFormfactorDefinition = {
name: 'GENERIC',
description: `A generic pipeline`,
documentationUrl: `https://github.com/webgptorg/promptbook/discussions/173`,
pipelineInterface: GENERIC_PIPELINE_INTERFACE,
};
/**
* Image generator is form of app that generates image from input message
*
* @public exported from `@promptbook/core`
*/
const ImageGeneratorFormfactorDefinition = {
name: 'IMAGE_GENERATOR',
description: `Generates prompt for image generation from input message`,
documentationUrl: `https://github.com/webgptorg/promptbook/discussions/184`,
pipelineInterface: {
inputParameters: [
{
name: 'inputMessage',
description: `Input message to be image made from`,
isInput: true,
isOutput: false,
},
],
outputParameters: [
{
name: 'prompt',
description: `Prompt to be used for image generation`,
isInput: false,
isOutput: true,
},
],
},
};
/**
* Matcher is form of app that evaluates (spreadsheet) content against defined criteria or patterns,
* determining if it matches or meets specific requirements. Used for classification,
* validation, filtering, and quality assessment of inputs.
*
* @public exported from `@promptbook/core`
*/
const MatcherFormfactorDefinition = {
name: 'EXPERIMENTAL_MATCHER',
description: spacetrim.spaceTrim(`
An evaluation system that determines whether content meets specific criteria or patterns.
Used for content validation, quality assessment, and intelligent filtering tasks. Currently in experimental phase.
`),
documentationUrl: `https://github.com/webgptorg/promptbook/discussions/177`,
pipelineInterface: {
inputParameters: [
/* Input parameters for content to be matched and criteria to match against */
{
name: 'nonce',
description: 'Just to prevent EXPERIMENTAL_MATCHER to be set as implicit formfactor',
isInput: true,
isOutput: false,
},
],
outputParameters: [
/* Output parameters containing match results, confidence scores, and relevant metadata */
],
},
};
/**
* Sheets is form of app that processes tabular data in CSV format, allowing transformation
* and analysis of structured data through AI-powered operations
*
* @public exported from `@promptbook/core`
*/
const SheetsFormfactorDefinition = {
name: 'SHEETS',
aliasNames: ['SHEETS', 'SHEET'],
description: `A formfactor for processing spreadsheet-like data in CSV format, enabling AI transformations on tabular data`,
documentationUrl: `https://github.com/webgptorg/promptbook/discussions/176`,
pipelineInterface: {
inputParameters: [
{
name: 'inputSheet',
description: `Input sheet to be processed as csv`,
isInput: true,
isOutput: false,
},
],
outputParameters: [
{
name: 'outputSheet',
description: `Output sheet as csv`,
isInput: false,
isOutput: true,
},
],
},
};
/**
* Translator is form of app that transforms input text from one form to another,
* such as language translation, style conversion, tone modification, or other text transformations.
*
* @public exported from `@promptbook/core`
*/
const TranslatorFormfactorDefinition = {
name: 'TRANSLATOR',
description: spacetrim.spaceTrim(`
A text transformation system that converts input content into different forms,
including language translations, paraphrasing, style conversions, and tone adjustments.
This form factor takes one input and produces one transformed output.
`),
documentationUrl: `https://github.com/webgptorg/promptbook/discussions/175`,
pipelineInterface: {
inputParameters: [
{
name: 'inputMessage',
description: `Input message to be translated`,
isInput: true,
isOutput: false,
},
],
outputParameters: [
{
name: 'outputMessage',
description: `Translated output message`,
isInput: false,
isOutput: true,
},
],
// <- TODO: [🤓] Maybe add {summary}
// <- TODO: [🧠] maybe change to {inputText}, {inputText} / or make system for any name of first input and first outpur parameter
},
};
/**
* All available formfactor definitions
*
* @public exported from `@promptbook/core`
*/
const FORMFACTOR_DEFINITIONS = [
GenericFormfactorDefinition,
ChatbotFormfactorDefinition,
TranslatorFormfactorDefinition,
SheetsFormfactorDefinition,
MatcherFormfactorDefinition,
GeneratorFormfactorDefinition,
ImageGeneratorFormfactorDefinition,
CompletionFormfactorDefinition,
// <- [🛬] When making new formfactor, copy the _boilerplate and link it here
];
// Note: [💞] Ignore a discrepancy between file name and entity name
/**
* Parses the formfactor command
*
* Note: This command is used as a formfactor for new commands and defines the app type format - it should NOT be used in any `.book` file
*
* @see `documentationUrl` for more details
*
* @public exported from `@promptbook/editable`
*/
const formfactorCommandParser = {
/**
* Name of the command
*/
name: 'FORMFACTOR',
/**
* Aliases for the FORMFACTOR command
*/
aliasNames: ['FORM', 'FF'],
/**
* FORMFACTOR command can be used in:
*/
isUsedInPipelineHead: true,
isUsedInPipelineTask: false,
/**
* Description of the FORMFACTOR command
*/
description: `Specifies the application type and interface requirements that this promptbook should conform to`,
/**
* Link to documentation
*/
documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/168',
/**
* Example usages of the FORMFACTOR command
*/
examples: ['FORMFACTOR Chatbot', 'FF Chat'],
/**
* Parses the FORMFACTOR command
*/
parse(input) {
const { args } = input;
if (args.length !== 1) {
throw new ParseError(`FORMFACTOR command requires exactly one argument`);
}
const formfactorNameCandidate = args[0].toUpperCase();
const formfactor = FORMFACTOR_DEFINITIONS.find((definition) => [definition.name, ...{ aliasNames: [], ...definition }.aliasNames].includes(formfactorNameCandidate));
if (formfactor === undefined) {
throw new ParseError(spacetrim.spaceTrim((block) => `
Unknown formfactor name "${formfactorNameCandidate}"
Available formfactors:
${block(FORMFACTOR_DEFINITIONS.map(({ name }) => `- ${name}`).join('\n'))}
`));
}
return {
type: 'FORMFACTOR',
formfactorName: formfactor.name,
};
},
/**
* Apply the FORMFACTOR command to the `pipelineJson`
*
* Note: `$` is used to indicate that this function mutates given `pipelineJson`
*/
$applyToPipelineJson(command, $pipelineJson) {
if ($pipelineJson.formfactorName !== undefined && $pipelineJson.formfactorName !== command.formfactorName) {
throw new ParseError(spacetrim.spaceTrim(`
Redefinition of \`FORMFACTOR\` in the pipeline head
You have used:
1) FORMFACTOR \`${$pipelineJson.formfactorName}\`
2) FORMFACTOR \`${command.formfactorName}\`
`));
}
$pipelineJson.formfactorName = command.formfactorName;
},
/**
* Converts the FORMFACTOR command back to string
*
* Note: This is used in `pipelineJsonToString` utility
*/
stringify(command) {
return `FORMFACTOR ${command.formfactorName}`;
},
/**
* Reads the FORMFACTOR command from the `PipelineJson`
*
* Note: This is used in `pipelineJsonToString` utility
*/
takeFromPipelineJson(pipelineJson) {
return [
{
type: 'FORMFACTOR',
formfactorName: pipelineJson.formfactorName,
},
];
},
};
/**
* Parses the joker command
*
* @see `documentationUrl` for more details
*
* @public exported from `@promptbook/editable`
*/
const jokerCommandParser = {
/**
* Name of the command
*/
name: 'JOKER',
/**
* BOILERPLATE command can be used in:
*/
isUsedInPipelineHead: false,
isUsedInPipelineTask: true,
/**
* Description of the JOKER command
*/
description: `Joker parameter is used instead of executing the task result if jokers value meets the expectations requirements`,
/**
* Link to documentation
*/
documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/66',
/**
* Example usages of the JOKER command
*/
examples: ['JOKER {documentTitle}'],
/**
* Parses the JOKER command
*/
parse(input) {
const { args } = input;
if (args.length !== 1) {
throw new ParseError(`JOKE command expects exactly one parameter name`);
}
const parameterNameArg = args[0] || '';
const parameterName = validateParameterName(parameterNameArg);
return {
type: 'JOKER',
parameterName,
};
},
/**
* Apply the JOKER command to the `pipelineJson`
*
* Note: `$` is used to indicate that this function mutates given `taskJson`
*/
$applyToTaskJson(command, $taskJson) {
$taskJson.jokerParameterNames = $taskJson.jokerParameterNames || [];
$taskJson.jokerParameterNames.push(command.parameterName);
},
/**
* Converts the JOKER command back to string
*
* Note: This is used in `pipelineJsonToString` utility
*/
stringify(command) {
return `---`; // <- TODO: [🛋] Implement
},
/**
* Reads the JOKER command from the `TaskJson`
*
* Note: This is used in `pipelineJsonToString` utility
*/
takeFromTaskJson($taskJson) {
throw new NotYetImplementedError(`[🛋] Not implemented yet`); // <- TODO: [🛋] Implement
},
};
/**
* 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(/\r?\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
/**
* Converts a given text to kebab-case format.
*
* Note: [🔂] This function is idempotent.
*
* @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 = CryptoJS__default["default"].SHA256(hexEncoder__default["default"].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.spaceTrim(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 [⛱])*
/**
* Constant for model variants.
*
* @see {@link ModelVariant}
*
* @public exported from `@promptbook/core`
*/
const MODEL_VARIANTS = [
'COMPLETION',
'CHAT',
'IMAGE_GENERATION',
'EMBEDDING' /* <- TODO [🏳] */ /* <- [🤖] */,
];
/**
* Parses the model command
*
* @see `documentationUrl` for more details
* @deprecated Option to manually set the model requirements is not recommended to use, use `PERSONA` instead
*
* @public exported from `@promptbook/editable`
*/
const modelCommandParser = {
/**
* Name of the command
*/
name: 'MODEL',
/**
* BOILERPLATE command can be used in:
*/
isUsedInPipelineHead: true,
isUsedInPipelineTask: true,
/**
* Description of the MODEL command
*/
description: `Tells which \`modelRequirements\` (for example which model) to use for the prompt task execution`,
/**
* Link to documentation
*/
documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/67',
/**
* Example usages of the MODEL command
*/
examples: ['MODEL VARIANT Chat', 'MODEL NAME `gpt-4`'],
/**
* Parses the MODEL command
*/
parse(input) {
const { args, normalized } = input;
const availableVariantsMessage = spacetrim.spaceTrim((block) => `
Available variants are:
${block(MODEL_VARIANTS.map((variantName) => `- ${variantName}${variantName !== 'EMBEDDING' ? '' : ' (Not available in pipeline)'}`).join('\n'))}
`);
// TODO: Make this more elegant and dynamically
if (normalized.startsWith('MODEL_VARIANT')) {
if (normalized === 'MODEL_VARIANT_CHAT') {
return {
type: 'MODEL',
key: 'modelVariant',
value: 'CHAT',
};
}
else if (normalized === 'MODEL_VARIANT_COMPLETION') {
return {
type: 'MODEL',
key: 'modelVariant',
value: 'COMPLETION',
};
// <- Note: [🤖]
}
else if (normalized.startsWith('MODEL_VARIANT_EMBED')) {
spacetrim.spaceTrim((block) => `
Embedding model can not be used in pipeline
${block(availableVariantsMessage)}
`);
}
else {
throw new ParseError(spacetrim.spaceTrim((block) => `
Unknown model variant in command:
${block(availableVariantsMessage)}
`));
}
}
if (normalized.startsWith('MODEL_NAME')) {
return {
type: 'MODEL',
key: 'modelName',
value: args.pop(),
};
}
else {
throw new ParseError(spacetrim.spaceTrim((block) => `
Unknown model key in command.
Supported model keys are:
${block(['variant', 'name'].join(', '))}
Example:
- MODEL VARIANT Chat
- MODEL NAME gpt-4
`));
}
},
/**
* Apply the MODEL command to the `pipelineJson`
*
* Note: `$` is used to indicate that this function mutates given `pipelineJson`
*/
$applyToPipelineJson(command, $pipelineJson) {
$pipelineJson.defaultModelRequirements = $pipelineJson.defaultModelRequirements || {};
// TODO: [🚜] DRY
if ($pipelineJson.defaultModelRequirements[command.key] !== undefined) {
if ($pipelineJson.defaultModelRequirements[command.key] === command.value) {
console.warn(`Multiple commands \`MODEL ${command.key} ${command.value}\` in the pipeline head`);
// <- TODO: [🏮] Some better way how to get warnings from pipeline parsing / logic
// <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
}
else {
throw new ParseError(spacetrim.spaceTrim(`
Redefinition of \`MODEL ${command.key}\` in the pipeline head
You have used:
1) \`MODEL ${command.key} ${$pipelineJson.defaultModelRequirements[command.key]}\`
2) \`MODEL ${command.key} ${command.value}\`
`));
}
}
$pipelineJson.defaultModelRequirements[command.key] = command.value;
},
/**
* Apply the MODEL command to the `pipelineJson`
*
* Note: `$` is used to indicate that this function mutates given `taskJson`
*/
$applyToTaskJson(command, $taskJson, $pipelineJson) {
if ($taskJson.taskType !== 'PROMPT_TASK') {
throw new ParseError(`MODEL command can only be used in PROMPT_TASK block`);
}
$taskJson.modelRequirements = $taskJson.modelRequirements || {};
// TODO: [🚜] DRY
if ($taskJson.modelRequirements[command.key] !== undefined) {
if ($taskJson.modelRequirements[command.key] === command.value) {
console.warn(`Multiple commands \`MODEL ${command.key} ${command.value}\` in the task "${$taskJson.title || $taskJson.name}"`);
// <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
}
else {
throw new ParseError(spacetrim.spaceTrim(`
Redefinition of MODEL \`${command.key}\` in the task "${$taskJson.title || $taskJson.name}"
You have used:
- MODEL ${command.key} ${$taskJson.modelRequirements[command.key]}
- MODEL ${command.key} ${command.value}
`));
}
}
if (command.value === ($pipelineJson.defaultModelRequirements || {})[command.key]) {
console.log(spacetrim.spaceTrim(`
Setting MODEL \`${command.key}\` in the task "${$taskJson.title || $taskJson.name}" to the same value as in the pipeline head
In pipeline head:
- MODEL ${command.key} ${($pipelineJson.defaultModelRequirements || {})[command.key]}
But same value is used in the task:
- MODEL ${command.key} ${command.value}
`));
}
$taskJson.modelRequirements[command.key] = command.value;
},
/**
* Converts the MODEL command back to string
*
* Note: This is used in `pipelineJsonToString` utility
*/
stringify(command) {
return `---`; // <- TODO: [🛋] Implement
},
/**
* Reads the MODEL command from the `PipelineJson`
*
* Note: This is used in `pipelineJsonToString` utility
*/
takeFromPipelineJson(pipelineJson) {
throw new NotYetImplementedError(`[🛋] Not implemented yet`); // <- TODO: [🛋] Implement
},
/**
* Reads the MODEL command from the `TaskJson`
*
* Note: This is used in `pipelineJsonToString` utility
*/
takeFromTaskJson($taskJson) {
throw new NotYetImplementedError(`[🛋] Not implemented yet`); // <- TODO: [🛋] Implement
},
};
/**
* Parses the parameter command
*
* @see `documentationUrl` for more details
*
* @public exported from `@promptbook/editable`
*/
const parameterCommandParser = {
/**
* Name of the command
*/
name: 'PARAMETER',
/**
* Aliases for the PARAMETER command
*/
aliasNames: ['PARAM', 'INPUT_PARAM', 'OUTPUT_PARAM', 'INPUT_PARAMETER', 'OUTPUT_PARAMETER'],
/*
Note: [🦈] No need for extra alias name because it is already preprocessed
*/
/**
* BOILERPLATE command can be used in:
*/
isUsedInPipelineHead: true,
isUsedInPipelineTask: true,
/**
* Description of the PARAMETER command
*/
description: `Describes one parameter of the task`,
/**
* Link to documentation
*/
documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/68',
/**
* Example usages of the PARAMETER command
*/
examples: ['PARAMETER {title} Title of the book', 'OUTPUT PARAMETER {websiteContent} Content of the book'],
/**
* Parses the PARAMETER command
*/
parse(input) {
const { normalized, args, raw } = input;
const parameterNameRaw = args.shift() || '';
const parameterDescriptionRaw = args.join(' ');
// <- TODO: When [🥶] fixed, change to:
// > const parameterDescriptionRaw = rawArgs.split(parameterNameRaw).join('').trim();
if (parameterDescriptionRaw && parameterDescriptionRaw.match(/\{(?<embeddedParameterName>[a-z0-9_]+)\}/im)) {
throw new ParseError(spacetrim.spaceTrim((block) => `
Parameter \`{${parameterNameRaw}}\` can not contain another parameter in description
The description:
${block(parameterDescriptionRaw)}
`));
}
let isInput = normalized.startsWith('INPUT');
let isOutput = normalized.startsWith('OUTPUT');
if (raw.startsWith('> {')) {
isInput = false;
isOutput = false;
}
const parameterName = validateParameterName(parameterNameRaw);
const parameterDescription = parameterDescriptionRaw.trim() || null;
return {
type: 'PARAMETER',
parameterName,
parameterDescription,
isInput,
isOutput,
};
},
/**
* Apply the PARAMETER command to the `pipelineJson`
*
* Note: `$` is used to indicate that this function mutates given `pipelineJson`
*/
$applyToPipelineJson(command, $pipelineJson) {
// Note: [🍣] Do nothing, its application is implemented separately in `parsePipeline`
},
/**
* Apply the PARAMETER command to the `pipelineJson`
*
* Note: `$` is used to indicate that this function mutates given `taskJson`
*/
$applyToTaskJson(command, $taskJson, $pipelineJson) {
// Note: [🍣] Do nothing, its application is implemented separately in `parsePipeline`
},
/**
* Converts the PARAMETER command back to string
*
* Note: This is used in `pipelineJsonToString` utility
*/
stringify(command) {
return `---`; // <- TODO: [🛋] Implement
},
/**
* Reads the PARAMETER command from the `PipelineJson`
*
* Note: This is used in `pipelineJsonToString` utility
*/
takeFromPipelineJson(pipelineJson) {
throw new NotYetImplementedError(`[🛋] Not implemented yet`); // <- TODO: [🛋] Implement
},
/**
* Reads the PARAMETER command from the `TaskJson`
*
* Note: This is used in `pipelineJsonToString` utility
*/
takeFromTaskJson($taskJson) {
throw new NotYetImplementedError(`[🛋] Not implemented yet`); // <- TODO: [🛋] Implement
},
};
/**
* Parses the persona command
*
* @see `documentationUrl` for more details
*
* @public exported from `@promptbook/editable`
*/
const personaCommandParser = {
/**
* Name of the command
*/
name: 'PERSONA',
/**
* Aliases for the PERSONA command
*/
aliasNames: ['PERSON'],
/**
* PERSONA command can be used in:
*/
isUsedInPipelineHead: true,
isUsedInPipelineTask: true,
/**
* Description of the PERSONA command
*/
description: `Persona command is used to specify who the system is, it will be transformed into system message, top_t,...`,
/**
* Link to documentation
*/
documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/22',
/**
* Example usages of the PERSONA command
*/
examples: ['PERSONA Jane, skilled copywriter', 'PERSONA Joe, male 28 years old, programmer'],
/**
* Parses the PERSONA command
*/
parse(input) {
const { rawArgs } = input;
const [personaNameRaw, personaDescriptionRaw] = rawArgs.split(/[,;:]/, 2);
const personaName = (personaNameRaw || '').trim();
if (personaName === '') {
throw new ParseError(`You must set name for the persona`);
}
let personaDescription = (personaDescriptionRaw || '').trim();
if (personaDescription === '') {
personaDescription = null;
}
return {
type: 'PERSONA',
personaName,
personaDescription,
};
},
/**
* Apply the PERSONA command to the `pipelineJson`
*
* Note: `$` is used to indicate that this function mutates given `pipelineJson`
*/
$applyToPipelineJson(command, $pipelineJson) {
$applyToTaskJson(command, null, $pipelineJson);
},
$applyToTaskJson,
/**
* Converts the PERSONA command back to string
*
* Note: This is used in `pipelineJsonToString` utility
*/
stringify(command) {
return `---`; // <- TODO: [🛋] Implement
},
/**
* Reads the PERSONA command from the `PipelineJson`
*
* Note: This is used in `pipelineJsonToString` utility
*/
takeFromPipelineJson(pipelineJson) {
throw new NotYetImplementedError(`[🛋] Not implemented yet`); // <- TODO: [🛋] Implement
},
/**
* Reads the PERSONA command from the `TaskJson`
*
* Note: This is used in `pipelineJsonToString` utility
*/
takeFromTaskJson($taskJson) {
throw new NotYetImplementedError(`[🛋] Not implemented yet`); // <- TODO: [🛋] Implement
},
};
/**
* Apply the PERSONA command to the `pipelineJson`
*
* Note: `$` is used to indicate that this function mutates given `taskJson`
*/
function $applyToTaskJson(command, $taskJson, $pipelineJson) {
const { personaName, personaDescription } = command;
if ($taskJson !== null) {
if ($taskJson.taskType !== 'PROMPT_TASK') {
throw new ParseError(`PERSONA command can be used only in PROMPT_TASK block`);
}
$taskJson.personaName = personaName;
}
const persona = $pipelineJson.personas.find((persona) => persona.name === personaName);
if (persona === undefined) {
$pipelineJson.personas.push({
name: personaName,
description: personaDescription || '',
});
return;
}
if (persona.description === personaDescription) {
return;
}
if (personaDescription === null) {
return;
}
if (persona.description === '') {
persona.description = personaDescription;
return;
}
console.warn(spacetrim.spaceTrim(`
Persona "${personaName}" is defined multiple times with different description:
First definition:
${persona.description}
Second definition:
${personaDescription}
`));
persona.description += spacetrim.spaceTrim('\n\n' + personaDescription);
}
/**
* Checks if the given value is a valid JavaScript identifier name.
*
* @param javascriptName The value to check for JavaScript identifier validity.
* @returns `true` if the value is a valid JavaScript name, false otherwise.
*
* @public exported from `@promptbook/utils`
*/
function isValidJavascriptName(javascriptName) {
if (typeof javascriptName !== 'string') {
return false;
}
return /^[a-zA-Z_$][0-9a-zA-Z_$]*$/i.test(javascriptName);
}
/**
* Parses the postprocess command
*
* @see `documentationUrl` for more details
*
* @public exported from `@promptbook/editable`
*/
const postprocessCommandParser = {
/**
* Name of the command
*/
name: 'POSTPROCESS',
aliasNames: ['POSTPROCESSING', 'PP'],
/**
* BOILERPLATE command can be used in:
*/
isUsedInPipelineHead: false,
isUsedInPipelineTask: true,
/**
* Description of the POSTPROCESS command
*/
description: `Defines the postprocess function to be used on the result from LLM and before the result is validated`,
/**
* Link to documentation
*/
documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/31',
/**
* Example usages of the POSTPROCESS command
*/
examples: [
'POSTPROCESS unwrapResult' /* <- TODO: Make it `POSTPROCESS` examples dynamic, load from all possible postprocess functions */,
],
/**
* Parses the POSTPROCESS command
*/
parse(input) {
const { args } = input;
const functionName = args.pop();
if (functionName === undefined) {
throw new ParseError(`Postprocess function name is required`);
}
if (!isValidJavascriptName(functionName)) {
throw new ParseError(`Invalid postprocess function name "${functionName}"`);
}
if (args.length > 0) {
throw new ParseError(`Can not have more than one postprocess function`);
}
return {
type: 'POSTPROCESS',
functionName,
};
},
/**
* Apply the POSTPROCESS command to the `pipelineJson`
*
* Note: `$` is used to indicate that this function mutates given `taskJson`
*/
$applyToTaskJson(command, $taskJson) {
$taskJson.postprocessingFunctionNames = $taskJson.postprocessingFunctionNames || [];
$taskJson.postprocessingFunctionNames.push(command.functionName);
},
/**
* Converts the POSTPROCESS command back to string
*
* Note: This is used in `pipelineJsonToString` utility
*/
stringify(command) {
return `---`; // <- TODO: [🛋] Implement
},
/**
* Reads the POSTPROCESS command from the `TaskJson`
*
* Note: This is used in `pipelineJsonToString` utility
*/
takeFromTaskJson($taskJson) {
throw new NotYetImplementedError(`[🛋] Not implemented yet`); // <- TODO: [🛋] Implement
},
};
/**
* 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,
];
/**
* 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.spaceTrim((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.spaceTrim(`
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
* ```
*/
/**
* Tests if given string is valid pipeline URL URL.
*
* Note: There are few similar functions:
* - `isValidUrl` which tests any URL
* - `isValidAgentUrl` which tests just agent URL
* - `isValidPipelineUrl` *(this one)* which tests just pipeline URL
*
* @public exported from `@promptbook/utils`
*/
function isValidPipelineUrl(url) {
if (!isValidUrl(url)) {
return false;
}
if (!url.startsWith('https://') && !url.startsWith('http://') /* <- Note: [👣] */) {
return false;
}
if (url.includes('#')) {
// TODO: [🐠]
return false;
}
/*
Note: [👣][🧠] Is it secure to allow pipeline URLs on private and unsecured networks?
if (isUrlOnPrivateNetwork(url)) {
return false;
}
*/
return true;
}
// TODO: [🐠] Maybe more info why the URL is invalid
/**
* Parses the url command
*
* @see `documentationUrl` for more details
*
* @public exported from `@promptbook/editable`
*/
const urlCommandParser = {
/**
* Name of the command
*/
name: 'URL',
aliasNames: ['PIPELINE_URL'],
/*
Note: [🛵] No need for this alias name because it is already preprocessed
aliasNames: ['HTTPS'],
*/
/**
* BOILERPLATE command can be used in:
*/
isUsedInPipelineHead: true,
isUsedInPipelineTask: false,
/**
* Description of the URL command
*/
description: `Declares unique URL for the pipeline`,
/**
* Link to documentation
*/
documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/70',
/**
* Example usages of the URL command
*/
examples: [
'PIPELINE URL https://promptbook.studio/library/write-cv.book',
'URL https://promptbook.studio/library/write-cv.book',
'https://promptbook.studio/library/write-cv.book',
],
/**
* Parses the URL command
*/
parse(input) {
const { args } = input;
const pipelineUrl = args.pop();
if (pipelineUrl === undefined) {
throw new ParseError(`URL is required`);
}
// TODO: [🧠][🚲] This should be maybe tested as logic not parse
if (!isValidPipelineUrl(pipelineUrl)) {
throw new ParseError(`Invalid pipeline URL "${pipelineUrl}"`);
}
if (args.length > 0) {
throw new ParseError(`Can not have more than one pipeline URL`);
}
/*
TODO: [🐠 Maybe more info from `isValidPipelineUrl`:
if (pipelineUrl.protocol !== 'https:') {
throw new ParseError(`Protocol must be HTTPS`);
}
if (pipelineUrl.hash !== '') {
throw new ParseError(
spaceTrim(
`
URL must not contain hash
Hash is used for identification of the section of the pipeline
`,
),
);
}
*/
return {
type: 'URL',
pipelineUrl: new URL(pipelineUrl),
};
},
/**
* Apply the URL command to the `pipelineJson`
*
* Note: `$` is used to indicate that this function mutates given `pipelineJson`
*/
$applyToPipelineJson(command, $pipelineJson) {
$pipelineJson.pipelineUrl = command.pipelineUrl.href;
},
/**
* Converts the URL command back to string
*
* Note: This is used in `pipelineJsonToString` utility
*/
stringify(command) {
return `---`; // <- TODO: [🛋] Implement
},
/**
* Reads the URL command from the `PipelineJson`
*
* Note: This is used in `pipelineJsonToString` utility
*/
takeFromPipelineJson(pipelineJson) {
throw new NotYetImplementedError(`[🛋] Not implemented yet`); // <- TODO: [🛋] Implement
},
};
/**
* Parses the action command
*
* @see `documentationUrl` for more details
*
* @public exported from `@promptbook/editable`
*/
const actionCommandParser = {
/**
* Name of the command
*/
name: 'ACTION',
/**
* ACTION command can be used in:
*/
isUsedInPipelineHead: true,
isUsedInPipelineTask: false,
/**
* Description of the ACTION command
*/
description: `Actions influences from the pipeline or task into external world. Like turning on a light, sending an email, etc.`,
/**
* Link to documentation
*/
documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/72',
/**
* Example usages of the ACTION command
*/
examples: ['ACTION'],
/**
* Parses the ACTION command
*/
parse(input) {
return {
type: 'ACTION',
};
},
/**
* Apply the ACTION command to the `pipelineJson`
*
* Note: `$` is used to indicate that this function mutates given `pipelineJson`
*/
$applyToPipelineJson(command, $pipelineJson) {
console.error(new NotYetImplementedError('[🛠] Actions are not implemented yet'));
},
/**
* Converts the ACTION command back to string
*
* Note: This is used in `pipelineJsonToString` utility
*/
stringify(command) {
throw new NotYetImplementedError('[🛠] Actions are not implemented yet');
},
/**
* Reads the ACTION command from the `PipelineJson`
*
* Note: This is used in `pipelineJsonToString` utility
*/
takeFromPipelineJson(pipelineJson) {
throw new NotYetImplementedError('[🛠] Actions are not implemented yet');
},
};
// Note: [⛱] There are two types of ACTION commands *...(read more in [⛱])*
/**
* Parses the instrument command
*
* @see `documentationUrl` for more details
*
* @public exported from `@promptbook/editable`
*/
const instrumentCommandParser = {
/**
* Name of the command
*/
name: 'INSTRUMENT',
/**
* INSTRUMENT command can be used in:
*/
isUsedInPipelineHead: true,
isUsedInPipelineTask: false,
/**
* Description of the INSTRUMENT command
*/
description: `Instrument command is used to specify the instrument to be used in the pipeline or task like search, calculate, etc.`,
/**
* Link to documentation
*/
documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/71',
/**
* Example usages of the INSTRUMENT command
*/
examples: ['INSTRUMENT'],
/**
* Parses the INSTRUMENT command
*/
parse(input) {
return {
type: 'INSTRUMENT',
};
},
/**
* Apply the INSTRUMENT command to the `pipelineJson`
*
* Note: `$` is used to indicate that this function mutates given `pipelineJson`
*/
$applyToPipelineJson(command, $pipelineJson) {
console.error(new NotYetImplementedError('[🛠] Instruments are not implemented yet'));
},
/**
* Converts the INSTRUMENT command back to string
*
* Note: This is used in `pipelineJsonToString` utility
*/
stringify(command) {
throw new NotYetImplementedError('[🛠] Instruments are not implemented yet');
},
/**
* Reads the INSTRUMENT command from the `PipelineJson`
*
* Note: This is used in `pipelineJsonToString` utility
*/
takeFromPipelineJson(pipelineJson) {
throw new NotYetImplementedError('[🛠] Instruments are not implemented yet');
},
};
// Note: [⛱] There are two types of INSTRUMENT commands *...(read more in [⛱])*
/**
* All available command parsers
*
* @public exported from `@promptbook/editable`
*/
const COMMANDS = [
sectionCommandParser,
expectCommandParser,
formatCommandParser,
jokerCommandParser,
modelCommandParser,
parameterCommandParser,
postprocessCommandParser,
bookVersionCommandParser,
formfactorCommandParser,
urlCommandParser,
knowledgeCommandParser,
actionCommandParser,
instrumentCommandParser,
personaCommandParser,
foreachCommandParser,
boilerplateCommandParser, // <- TODO: Only in development, remove in production
// <- Note: [♓️][💩] This is the order of the commands in the pipeline, BUT its not used in parsing and before usage maybe it should be done better
];
// Note: [💞] Ignore a discrepancy between file name and entity name
/**
* Gets the parser for the command
*
* @returns the parser for the command
* @throws {UnexpectedError} if the parser is not found
*
* @public exported from `@promptbook/editable`
*/
function getParserForCommand(command) {
const commandParser = COMMANDS.find((commandParser) => commandParser.name === command.type);
if (commandParser === undefined) {
throw new UnexpectedError(spacetrim.spaceTrim((block) => `
Command ${command.type} parser is not found
${block(JSON.stringify(command, null, 4)
.split(/\r?\n/)
.map((line) => `> ${line}`)
.join('\n'))}
`));
}
return commandParser;
}
/**
* Removes Markdown formatting tags from a string.
*
* @param {string} str - The string to remove Markdown tags from.
* @returns {string} The input string with all Markdown tags removed.
*
* @public exported from `@promptbook/markdown-utils`
*/
function removeMarkdownFormatting(str) {
// Remove bold formatting
str = str.replace(/\*\*(.*?)\*\*/g, '$1');
// Remove italic formatting
str = str.replace(/\*(.*?)\*/g, '$1');
// Remove code formatting
str = str.replace(/`(.*?)`/g, '$1');
return str;
}
/**
* Parses one line of ul/ol to command
*
* @returns parsed command object
* @throws {ParseError} if the command is invalid
*
* @public exported from `@promptbook/editable`
*/
function parseCommand(raw, usagePlace) {
if (raw.includes('\n') || raw.includes('\r')) {
throw new ParseError('Command can not contain new line characters' /* <- TODO: [🚞] */);
}
// TODO: Unit test all this processing and parsing
let normalized = raw.trim();
normalized = normalized.split('`').join('');
normalized = normalized.split('"').join('');
normalized = normalized.split("'").join('');
normalized = normalized.split('~').join('');
normalized = normalized.split('[').join('');
normalized = normalized.split(']').join('');
normalized = normalized.split('(').join('');
normalized = normalized.split(')').join('');
normalized = normalizeTo_SCREAMING_CASE(normalized);
const items = raw
.trim()
// Note: [🐡]
.split(/^>/)
.join('')
// ---
.trim()
.split(/["'`]/)
.join('')
.trim()
// Note: [🛵]:
.split(/^http/)
.join('URL http')
// ---
// Note: [🦈]
.split(/^{/)
.join('PARAMETER {')
// ---
.split(' ')
.map((part) => part.trim())
.filter((item) => item !== '')
.map(removeMarkdownFormatting)
.map((item) => item.trim());
if (items.length === 0 || items[0] === '') {
throw new ParseError(spacetrim.spaceTrim((block) => `
Malformed command:
- ${raw}
Supported commands are:
${block(getSupportedCommandsMessage())}
`));
}
// Note: Taking command name from beginning of the line
// FOO | BAR Arg1 Arg2 Arg3
// FOO BAR | Arg1 Arg2 Arg3
for (let commandNameSegmentsCount = 0; commandNameSegmentsCount < Math.min(items.length, 3); commandNameSegmentsCount++) {
const commandNameRaw = items.slice(0, commandNameSegmentsCount + 1).join('_');
const args = items.slice(commandNameSegmentsCount + 1);
const rawArgs = raw
.substring(commandNameRaw.length)
.trim();
const command = parseCommandVariant({ usagePlace, raw, rawArgs, normalized, args, commandNameRaw });
if (command !== null) {
return command;
}
}
// Note: Taking command name from end of the line
// Arg1 Arg2 Arg3 | FOO
{
const commandNameRaw = items.slice(-1).join('_');
const args = items.slice(0, -1); // <- Note: This is probably not correct
const rawArgs = raw
.substring(0, raw.length - commandNameRaw.length)
.trim();
const command = parseCommandVariant({ usagePlace, raw, rawArgs, normalized, args, commandNameRaw });
if (command !== null) {
return command;
}
}
throw new ParseError(spacetrim.spaceTrim((block) => `
Malformed or unknown command:
- ${raw}
Supported commands are:
${block(getSupportedCommandsMessage())}
`));
}
/**
* Generates a markdown-formatted message listing all supported commands
* with their descriptions and documentation links
*
* @returns A formatted markdown string containing all available commands and their details
*/
function getSupportedCommandsMessage() {
return COMMANDS.flatMap(({ name, aliasNames, description, documentationUrl }) =>
// <- Note: [🦦] Its strange that this type assertion is needed
[
`- **${name}** ${description}, see [discussion](${documentationUrl})`,
...(aliasNames || []).map((aliasName) => ` - **${aliasName}** Alias for **${name}**`),
]).join('\n');
}
/**
* Attempts to parse a command variant using the provided input parameters
*
* @param input Object containing command parsing information including raw command text and normalized values
* @returns A parsed Command object if successful, or null if the command cannot be parsed
*/
function parseCommandVariant(input) {
const { commandNameRaw, usagePlace, normalized, args, raw, rawArgs } = input;
const commandName = normalizeTo_SCREAMING_CASE(commandNameRaw);
for (const commandParser of COMMANDS) {
// <- Note: [🦦] Its strange that this type assertion is needed
const { name, isUsedInPipelineHead, isUsedInPipelineTask, aliasNames, deprecatedNames, parse } = commandParser;
if (just(false)) ;
else if (usagePlace === 'PIPELINE_HEAD' && !isUsedInPipelineHead) {
continue;
}
else if (usagePlace === 'PIPELINE_TASK' && !isUsedInPipelineTask) {
continue;
}
const names = [name, ...(aliasNames || []), ...(deprecatedNames || [])];
if (names.includes(commandName)) {
try {
return parse({ usagePlace, raw, rawArgs, normalized, args }); // <- Note: [🦦]
}
catch (error) {
if (!(error instanceof ParseError)) {
throw error;
}
throw new ParseError(spacetrim.spaceTrim((block) => `
Invalid ${commandName} command:
Your command:
- ${raw}
The detailed error:
${block(error.message)}
Usage of ${commandName}:
${block(commandParser.examples.map((example) => `- ${example}`).join('\n'))}
All supported commands are:
${block(getSupportedCommandsMessage())}
`));
}
}
}
return null;
}
/**
* Utility function to extract all list items from markdown
*
* Note: It works with both ul and ol
* Note: It omits list items in code blocks
* Note: It flattens nested lists
* Note: It can not work with html syntax and comments
*
* @param markdown any valid markdown
* @returns An array of strings, each representing an individual list item found in the markdown
*
* @public exported from `@promptbook/markdown-utils`
*/
function extractAllListItemsFromMarkdown(markdown) {
const lines = markdown.split(/\r?\n/);
const listItems = [];
let isInCodeBlock = false;
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('```')) {
isInCodeBlock = !isInCodeBlock;
}
if (!isInCodeBlock && (trimmedLine.startsWith('-') || trimmedLine.match(/^\d+\./))) {
const listItem = trimmedLine.replace(/^-|\d+\./, '').trim();
listItems.push(listItem);
}
}
return listItems;
}
/**
* Builds a short file/url identification block for parse errors.
*
* @private internal utility of `parsePipeline`
*/
function getPipelineIdentification($pipelineJson) {
// Note: This is a 😐 implementation of [🚞]
const pipelineIdentificationParts = [];
if ($pipelineJson.sourceFile !== undefined) {
pipelineIdentificationParts.push(`File: ${$pipelineJson.sourceFile}`);
}
if ($pipelineJson.pipelineUrl !== undefined) {
pipelineIdentificationParts.push(`Url: ${$pipelineJson.pipelineUrl}`);
}
return pipelineIdentificationParts.join('\n');
}
/**
* Merges one parameter declaration into the mutable pipeline parameter list.
*
* @private internal utility of `parsePipeline`
*/
function defineParameter($pipelineJson, parameterCommand) {
const { parameterName, parameterDescription, isInput, isOutput } = parameterCommand;
if (RESERVED_PARAMETER_NAMES.includes(parameterName)) {
throw new ParseError(spacetrim.spaceTrim((block) => `
Parameter name {${parameterName}} is reserved and cannot be used as resulting parameter name
${block(getPipelineIdentification($pipelineJson))}
`) /* <- TODO: [🚞] */);
}
const existingParameter = $pipelineJson.parameters.find((parameter) => parameter.name === parameterName);
if (existingParameter &&
existingParameter.description &&
existingParameter.description !== parameterDescription &&
parameterDescription) {
throw new ParseError(spacetrim.spaceTrim((block) => `
Parameter \`{${parameterName}}\` is defined multiple times with different description:
${block(getPipelineIdentification($pipelineJson))}
First definition:
${block(existingParameter.description || '[undefined]')}
Second definition:
${block(parameterDescription || '[undefined]')}
`));
}
if (existingParameter) {
if (parameterDescription) {
existingParameter.description = parameterDescription;
}
existingParameter.isInput = existingParameter.isInput || isInput;
existingParameter.isOutput = existingParameter.isOutput || isOutput;
return;
}
$pipelineJson.parameters.push({
name: parameterName,
description: parameterDescription || undefined,
isInput,
isOutput,
});
}
/**
* Removes fenced code blocks when deriving human-readable section descriptions.
*
* @private internal utility of `extractPipelineDescription`
*/
const DESCRIPTION_CODE_BLOCK_REGEXP = /^```.*^```/gms;
/**
* Removes blockquote lines when deriving human-readable section descriptions.
*
* @private internal utility of `extractPipelineDescription`
*/
const DESCRIPTION_BLOCKQUOTE_REGEXP = /^>.*$/gm;
/**
* Removes list items and return statements when deriving human-readable section descriptions.
*
* @private internal utility of `extractPipelineDescription`
*/
const DESCRIPTION_LIST_ITEM_REGEXP = /^(?:(?:-)|(?:\d\))|(?:`?->))\s+.*$/gm;
/**
* Extracts the plain-text description from a head or task section body.
*
* @private internal utility of `parsePipeline`
*/
function extractPipelineDescription(sectionContent) {
let description = sectionContent;
description = description.split(DESCRIPTION_CODE_BLOCK_REGEXP).join('');
description = description.split(DESCRIPTION_BLOCKQUOTE_REGEXP).join('');
description = description.split(DESCRIPTION_LIST_ITEM_REGEXP).join('');
description = spacetrim.spaceTrim(description);
if (description === '') {
return undefined;
}
return description;
}
/**
* Applies the pipeline head title, description, and head-level commands.
*
* @private internal utility of `parsePipeline`
*/
function applyPipelineHead(pipelineHead, $pipelineJson) {
$pipelineJson.title = pipelineHead.title;
$pipelineJson.description = extractPipelineDescription(pipelineHead.content);
for (const listItem of extractAllListItemsFromMarkdown(pipelineHead.content)) {
applyPipelineHeadCommand(listItem, $pipelineJson);
}
}
/**
* Parses and applies one command declared in the pipeline head.
*
* @private internal utility of `applyPipelineHead`
*/
function applyPipelineHeadCommand(listItem, $pipelineJson) {
const command = parseCommand(listItem, 'PIPELINE_HEAD');
const commandParser = getParserForCommand(command);
if (commandParser.isUsedInPipelineHead !== true /* <- Note: [🦦][4] */) {
throw new ParseError(spacetrim.spaceTrim((block) => `
Command \`${command.type}\` is not allowed in the head of the pipeline ONLY at the pipeline task
${block(getPipelineIdentification($pipelineJson))}
`)); // <- TODO: [🚞]
}
try {
commandParser.$applyToPipelineJson(command, $pipelineJson);
// <- Note: [🦦] Its strange that this assertion must be here, [🦦][4] should do this assertion implicitly
}
catch (error) {
if (!(error instanceof ParseError)) {
throw error;
}
throw new ParseError(spacetrim.spaceTrim((block) => `
Command ${command.type} failed to apply to the pipeline
The error:
${block(error.message)}
Raw command:
- ${listItem}
Usage of ${command.type}:
${block(commandParser.examples.map((example) => `- ${example}`).join('\n'))}
${block(getPipelineIdentification($pipelineJson))}
`)); // <- TODO: [🚞]
}
if (command.type === 'PARAMETER') {
defineParameter($pipelineJson, command);
// <- Note: [🍣]
}
}
/**
* Creates the mutable pipeline JSON structure used throughout parsing.
*
* @private internal utility of `parsePipeline`
*/
function createInitialPipelineJson(pipelineString) {
return {
title: DEFAULT_BOOK_TITLE,
parameters: [],
tasks: [],
knowledgeSources: [],
knowledgePieces: [],
personas: [],
preparations: [],
sources: [
{
type: 'BOOK',
path: null,
// <- TODO: !!6 Pass here path of the file
content: pipelineString,
},
],
};
}
/**
* Converts a title string into a normalized name.
*
* Note: [🔂] This function is idempotent.
*
* @param value The title string to be converted to a name.
* @returns A normalized name derived from the input title.
* @example 'Hello World!' -> 'hello-world'
*
* @public exported from `@promptbook/utils`
*/
function titleToName(value) {
if (isValidUrl(value)) {
value = value.replace(/^https?:\/\//, '');
value = value.replace(/\.html$/, '');
}
else if (isValidFilePath(value)) {
value = path.basename(value);
// Note: Keeping extension in the name
}
value = value.split('/').join('-');
value = removeEmojis(value);
value = normalizeToKebabCase(value);
// TODO: [🧠] Maybe warn or add some padding to short name which are not good identifiers
return value;
}
/**
* Creates stable unique task names for duplicate section titles.
*
* @private internal utility of `parsePipeline`
*/
function createUniqueSectionNameResolver(pipelineSections) {
const sectionCounts = {};
for (const pipelineSection of pipelineSections) {
const sectionName = titleToName(pipelineSection.title);
if (sectionCounts[sectionName] === undefined) {
sectionCounts[sectionName] = { count: 0, currentIndex: 0 };
}
sectionCounts[sectionName].count++;
}
return (title) => {
const sectionName = titleToName(title);
const sectionCount = sectionCounts[sectionName];
if (sectionCount.count === 1) {
return sectionName;
}
const nameWithSuffix = `${sectionName}-${sectionCount.currentIndex}`;
sectionCount.currentIndex++;
return nameWithSuffix;
};
}
/**
* Extracts the interface (input and output parameters) from a pipeline.
*
* @deprecated https://github.com/webgptorg/promptbook/pull/186
* @see https://github.com/webgptorg/promptbook/discussions/171
*
* @public exported from `@promptbook/core`
*/
function getPipelineInterface(pipeline) {
const pipelineInterface = {
inputParameters: [],
outputParameters: [],
};
for (const parameter of pipeline.parameters) {
const { isInput, isOutput } = parameter;
if (isInput) {
pipelineInterface.inputParameters.push(deepClone(parameter));
}
if (isOutput) {
pipelineInterface.outputParameters.push(deepClone(parameter));
}
}
for (const key of ['inputParameters', 'outputParameters']) {
pipelineInterface[key].sort(({ name: name1 }, { name: name2 }) => name1.localeCompare(name2));
}
return exportJson({
name: `pipelineInterface`,
message: `Result of \`getPipelineInterface\``,
order: ['inputParameters', 'outputParameters'],
value: pipelineInterface,
});
}
/**
* Checks if two pipeline interfaces are structurally identical.
*
* @deprecated https://github.com/webgptorg/promptbook/pull/186
* @see https://github.com/webgptorg/promptbook/discussions/171
*
* @public exported from `@promptbook/core`
*/
function isPipelineInterfacesEqual(pipelineInterface1, pipelineInterface2) {
for (const whichParameters of ['inputParameters', 'outputParameters']) {
const parameters1 = pipelineInterface1[whichParameters]; // <- Note: `isPipelineInterfacesEqual` is just temporary solution, no need to fix this
const parameters2 = pipelineInterface2[whichParameters];
if (parameters1.length !== parameters2.length) {
return false;
}
for (const parameter of parameters1) {
const matchingParameter = parameters2.find(({ name }) => name === parameter.name);
if (!matchingParameter) {
return false;
}
// Note: Do not compare description, it is not relevant for compatibility
if (matchingParameter.isInput !== parameter.isInput) {
return false;
}
if (matchingParameter.isOutput !== parameter.isOutput) {
return false;
}
}
}
return true;
}
/**
* Checks if a given pipeline satisfies the requirements of a specified pipeline interface.
*
* @deprecated https://github.com/webgptorg/promptbook/pull/186
* @see https://github.com/webgptorg/promptbook/discussions/171
* @returns `true` if the pipeline implements the interface, `false` otherwise.
*
* @public exported from `@promptbook/core`
*/
function isPipelineImplementingInterface(options) {
const { pipeline, pipelineInterface } = options;
return isPipelineInterfacesEqual(getPipelineInterface(pipeline), pipelineInterface);
}
/**
* Set formfactor based on the pipeline interface e
*
* @private
*/
const ImplicitFormfactorHla = {
type: 'SYNC',
$applyToPipelineJson($pipelineJson) {
if ($pipelineJson.formfactorName !== undefined) {
// Note: When formfactor is already set, do nothing
return;
}
for (const formfactorDefinition of FORMFACTOR_DEFINITIONS.filter(({ name }) => name !== 'GENERIC')) {
// <- Note: [♓️][💩] This is the order of the formfactors, make some explicit priority
const { name, pipelineInterface } = formfactorDefinition;
const isCompatible = isPipelineImplementingInterface({
pipeline: {
formfactorName: name,
// <- Note: `formfactorName` has no role in `isPipelineImplementingInterface`
// but it is needed to satisfy the typescript
...$pipelineJson,
},
pipelineInterface,
});
/*/
console.log({
subject: `${$pipelineJson.title} implements ${name}`,
pipelineTitle: $pipelineJson.title,
formfactorName: name,
isCompatible,
formfactorInterface: pipelineInterface,
pipelineInterface: getPipelineInterface($pipelineJson as PipelineJson),
});
/**/
if (isCompatible) {
$pipelineJson.formfactorName = name;
return;
}
}
},
};
/**
* Allow to define chatbot with no need to write full interface
*
* @private
*/
const QuickChatbotHla = {
type: 'SYNC',
$applyToPipelineJson($pipelineJson) {
if ($pipelineJson.tasks.length !== 0) {
// Note: When there are already tasks, do nothing
return;
}
if ($pipelineJson.parameters.length !== 0) {
// Note: When there are already parameters, do nothing
return;
}
if ($pipelineJson.personas.length === 0) {
// Note: When no personas defined, do nothing
return;
}
const personaName = $pipelineJson.personas[0].name;
$pipelineJson.formfactorName = 'CHATBOT';
$pipelineJson.parameters.push({
name: 'previousTitle',
description: 'Previous title of the conversation',
isInput: true,
isOutput: false,
}, {
name: 'previousConversationSummary',
description: 'Previous conversation summary',
isInput: true,
isOutput: false,
}, {
name: 'userMessage',
description: 'User message',
isInput: true,
isOutput: false,
}, {
name: 'title',
description: 'Title of the conversation',
isInput: false,
isOutput: true,
}, {
name: 'conversationSummary',
description: 'Summary of the conversation',
isInput: false,
isOutput: true,
}, {
name: 'chatbotResponse',
description: 'Chatbot response',
isInput: false,
isOutput: true,
exampleValues: [
'Hello, how can I help you?',
// <- TODO: [🧠] Make this dynamic, async, prepare-phase HLAs
],
});
$pipelineJson.tasks.push({
taskType: 'PROMPT_TASK',
name: 'create-an-answer',
title: 'Create an answer',
content: spacetrim.spaceTrim(`
Write a response to the user message:
**Question from user**
> {userMessage}
**Previous conversation**
> {previousConversationSummary}
`),
resultingParameterName: 'chatbotResponse',
personaName,
dependentParameterNames: [
'userMessage',
'previousConversationSummary' /* TODO: [🧠][📛], 'knowledge'*/,
],
// TODO: [🧠][📛] preparedContent: '{content}\n\n## Knowledge\n\n{knowledge}',
}, {
taskType: 'PROMPT_TASK',
name: 'summarize-the-conversation',
title: 'Summarize the conversation',
content: spacetrim.spaceTrim(`
Summarize the conversation in a few words:
## Rules
- Summarise the text of the conversation in a few words
- Convert the text to its basic idea
- Imagine you are writing the headline or subject line of an email
- Respond with a few words of summary only
## Conversation
**User:**
> {userMessage}
**You:**
> {chatbotResponse}
`),
resultingParameterName: 'conversationSummary',
personaName,
expectations: {
words: {
min: 1,
max: 10,
},
},
dependentParameterNames: ['userMessage', 'chatbotResponse' /* TODO: [🧠][📛], 'knowledge'*/],
// TODO: [🧠][📛] preparedContent: '{content}\n\n## Knowledge\n\n{knowledge}',
}, {
taskType: 'SIMPLE_TASK',
name: 'title',
title: 'Title',
content: '{conversationSummary}',
resultingParameterName: 'title',
dependentParameterNames: ['conversationSummary' /* TODO: [🧠][📛], 'knowledge'*/],
// TODO: [🧠][📛] preparedContent: '{content}\n\n## Knowledge\n\n{knowledge}',
});
},
};
// TODO: [🧠][📛] Should this be here?
/**
* All high-level abstractions
*
* @private internal index of `parsePipeline` (= used for sync) and `preparePipeline` (= used for async)
*/
const HIGH_LEVEL_ABSTRACTIONS = [
ImplicitFormfactorHla,
QuickChatbotHla,
// <- Note: [♓️][💩] This is the order of the application of high-level abstractions application on pipeline JSON
];
// TODO: Test that all sync high-level abstractions are before async high-level abstractions
// Note: [💞] Ignore a discrepancy between file name and entity name
/**
* Applies postprocessing and exports the parsed pipeline JSON.
*
* @private internal utility of `parsePipeline`
*/
function finalizeParsedPipeline($pipelineJson) {
applyImplicitParameterDirections($pipelineJson);
removeUndefinedValuesFromPipeline($pipelineJson);
applySyncHighLevelAbstractions($pipelineJson);
ensurePipelineFormfactor($pipelineJson);
return exportParsedPipelineJson($pipelineJson);
}
/**
* Applies default INPUT/OUTPUT flags when the author did not specify them explicitly.
*
* @private internal utility of `finalizeParsedPipeline`
*/
function applyImplicitParameterDirections($pipelineJson) {
markImplicitInputParameters($pipelineJson);
markImplicitOutputParameters($pipelineJson);
}
/**
* Marks non-result parameters as pipeline inputs when no input was declared.
*
* @private internal utility of `finalizeParsedPipeline`
*/
function markImplicitInputParameters($pipelineJson) {
if ($pipelineJson.parameters.some((parameter) => parameter.isInput)) {
return;
}
for (const parameter of $pipelineJson.parameters) {
const isThisParameterResulting = $pipelineJson.tasks.some((task) => task.resultingParameterName === parameter.name);
if (!isThisParameterResulting) {
parameter.isInput = true;
// <- TODO: [💔] Why this is making typescript error in vscode but not in cli
// > Type 'true' is not assignable to type 'false'.ts(2322)
// > (property) isInput: false
// > The parameter is input of the pipeline The parameter is NOT input of the pipeline
}
}
}
/**
* Marks every non-input parameter as output when no output was declared.
*
* @private internal utility of `finalizeParsedPipeline`
*/
function markImplicitOutputParameters($pipelineJson) {
if ($pipelineJson.parameters.some((parameter) => parameter.isOutput)) {
return;
}
for (const parameter of $pipelineJson.parameters) {
if (!parameter.isInput) {
parameter.isOutput = true;
// <- TODO: [💔]
}
}
}
/**
* Removes `undefined` properties from serialized tasks and parameters.
*
* @private internal utility of `finalizeParsedPipeline`
*/
function removeUndefinedValuesFromPipeline($pipelineJson) {
$pipelineJson.tasks.forEach(removeUndefinedProperties);
$pipelineJson.parameters.forEach(removeUndefinedProperties);
}
/**
* Deletes all own properties with `undefined` values from a mutable JSON entity.
*
* @private internal utility of `finalizeParsedPipeline`
*/
function removeUndefinedProperties(entity) {
for (const [key, value] of Object.entries(entity)) {
if (value === undefined) {
delete entity[key];
}
}
}
/**
* Applies all sync-only high-level abstractions after parsing.
*
* @private internal utility of `finalizeParsedPipeline`
*/
function applySyncHighLevelAbstractions($pipelineJson) {
for (const highLevelAbstraction of HIGH_LEVEL_ABSTRACTIONS.filter(({ type }) => type === 'SYNC')) {
highLevelAbstraction.$applyToPipelineJson($pipelineJson);
}
}
/**
* Ensures parsed pipelines always have the default `GENERIC` formfactor.
*
* @private internal utility of `finalizeParsedPipeline`
*/
function ensurePipelineFormfactor($pipelineJson) {
// Note: [🔆] If formfactor is still not set, set it to 'GENERIC'
if ($pipelineJson.formfactorName === undefined) {
$pipelineJson.formfactorName = 'GENERIC';
}
}
/**
* Finalizes ordering and exports the parsed pipeline JSON.
*
* @private internal utility of `finalizeParsedPipeline`
*/
function exportParsedPipelineJson($pipelineJson) {
return exportJson({
name: 'pipelineJson',
message: `Result of \`parsePipeline\``,
order: ORDER_OF_PIPELINE_JSON,
value: {
formfactorName: 'GENERIC',
// <- Note: [🔆] Setting `formfactorName` is redundant to satisfy the typescript
...$pipelineJson,
},
});
}
// TODO: Use spaceTrim more effectively
// TODO: [🧠] Parameter flags - isInput, isOutput, isInternal
// TODO: [♈] Probably move expectations from tasks to parameters
// TODO: [🍙] Make some standard order of json properties
/**
* Parses markdown section to title its level and content
*
* @public exported from `@promptbook/markdown-utils`
*/
function parseMarkdownSection(value) {
var _a, _b;
const lines = value.split(/\r?\n/);
if (!lines[0].startsWith('#')) {
throw new ParseError('Markdown section must start with heading');
}
const title = lines[0].replace(/^#+\s*/, '');
const level = (_b = (_a = lines[0].match(/^#+/)) === null || _a === void 0 ? void 0 : _a[0].length) !== null && _b !== void 0 ? _b : 0;
const content = spacetrim.spaceTrim(lines.slice(1).join('\n'));
if (level < 1 || level > 6) {
throw new ParseError('Markdown section must have heading level between 1 and 6');
}
return { title, level: level, content };
}
/**
* Note: [🕞] In past (commit 42086e1603cbed506482997c00a8ee979af0a247) there was much more
* sophisticated implementation of this function through parsing markdown into JSON structure
* and flattening the actual structure
* NOW we are working just with markdown string and its good enough
*/
/**
* Splits the markdown into sections by headings
*
* @public exported from `@promptbook/markdown-utils`
*/
function splitMarkdownIntoSections(markdown) {
const lines = markdown.split(/\r?\n/);
const sections = [];
// TODO: [🧽] DRY
let currentType = 'MARKDOWN';
let buffer = [];
const finishSection = () => {
if (buffer.length === 0) {
return;
}
let section = spacetrim.spaceTrim(buffer.join('\n'));
if (section === '') {
return;
}
if (!section.startsWith('#')) {
section = `# ${DEFAULT_BOOK_TITLE}\n\n${section}`;
}
sections.push(section);
buffer = [];
};
for (const line of lines) {
if (currentType === 'MARKDOWN') {
if (line.startsWith('#')) {
finishSection();
}
buffer.push(line);
if (line.startsWith('```')) {
currentType = 'CODE_BLOCK';
}
else if (line.includes('<!--')) {
currentType = 'COMMENT';
}
}
else if (currentType === 'CODE_BLOCK') {
buffer.push(line);
if (line.startsWith('```')) {
currentType = 'MARKDOWN';
}
}
else if (currentType === 'COMMENT') {
buffer.push(line);
if (line.includes('-->')) {
currentType = 'MARKDOWN';
}
}
}
finishSection();
return sections;
}
/**
* TODO: [🏛] This can be part of markdown builder
* Note: [🕞] In past (commit 42086e1603cbed506482997c00a8ee979af0a247) there was much more
* sophisticated implementation of this function through parsing markdown into JSON structure
* and flattening the actual structure
* NOW we are working just with markdown string and its good enough
*/
/**
* Splits the prepared markdown into the pipeline head and task sections.
*
* @private internal utility of `parsePipeline`
*/
function parsePreparedPipelineSections(pipelineString, $pipelineJson) {
const [pipelineHead, ...pipelineSections] = splitMarkdownIntoSections(pipelineString).map(parseMarkdownSection); /* <- Note: [🥞] */
assertPipelineSectionsStructure(pipelineHead, pipelineSections, $pipelineJson);
return {
pipelineHead,
pipelineSections,
};
}
/**
* Ensures the flattened markdown has exactly one h1 head followed by only h2 sections.
*
* @private internal utility of `parsePreparedPipelineSections`
*/
function assertPipelineSectionsStructure(pipelineHead, pipelineSections, $pipelineJson) {
if (pipelineHead === undefined) {
throw new UnexpectedError(spacetrim.spaceTrim((block) => `
Pipeline head is not defined
${block(getPipelineIdentification($pipelineJson))}
This should never happen, because the pipeline already flattened
`));
}
if (pipelineHead.level !== 1) {
throw new UnexpectedError(spacetrim.spaceTrim((block) => `
Pipeline head is not h1
${block(getPipelineIdentification($pipelineJson))}
This should never happen, because the pipeline already flattened
`));
}
if (!pipelineSections.every((pipelineSection) => pipelineSection.level === 2)) {
throw new UnexpectedError(spacetrim.spaceTrim((block) => `
Not every pipeline section is h2
${block(getPipelineIdentification($pipelineJson))}
This should never happen, because the pipeline already flattened
`));
}
}
/**
* Checks if value is valid email
*
* @public exported from `@promptbook/utils`
*/
function isValidEmail(email) {
if (typeof email !== 'string') {
return false;
}
if (email.split(/\r?\n/).length > 1) {
return false;
}
return /^.+@.+\..+$/.test(email);
}
/**
* Function `validatePipelineString` will validate the if the string is a valid pipeline string
* It does not check if the string is fully logically correct, but if it is a string that can be a pipeline string or the string looks completely different.
*
* Note: [🔂] This function is idempotent.
*
* @param {string} pipelineString the candidate for a pipeline string
* @returns {PipelineString} the same string as input, but validated as valid
* @throws {ParseError} if the string is not a valid pipeline string
*
* @public exported from `@promptbook/core`
*/
function validatePipelineString(pipelineString) {
if (isValidJsonString(pipelineString)) {
throw new ParseError('Expected a book, but got a JSON string');
}
else if (isValidUrl(pipelineString)) {
throw new ParseError(`Expected a book, but got just the URL "${pipelineString}"`);
}
else if (isValidFilePath(pipelineString)) {
throw new ParseError(`Expected a book, but got just the file path "${pipelineString}"`);
}
else if (isValidEmail(pipelineString)) {
throw new ParseError(`Expected a book, but got just the email "${pipelineString}"`);
}
// <- TODO: Implement the validation + add tests when the pipeline logic considered as invalid
return pipelineString;
}
// TODO: [🧠][🈴] Where is the best location for this file
/**
* Number of padding lines to add at the end of the book content
*
* @public exported from `@promptbook/core`
*/
const PADDING_LINES = 11;
/**
* A function that adds padding to the book content
*
* Note: [🔂] This function is idempotent.
*
* @public exported from `@promptbook/core`
*/
function padBook(content) {
if (!content) {
return '\n'.repeat(PADDING_LINES);
}
const lines = content.split(/\r?\n/);
let trailingEmptyLines = 0;
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i];
if (line === undefined) {
// Note: This should not happen in reality, but it's here to satisfy TypeScript's noUncheckedIndexedAccess option
continue;
}
if (line.trim() === '') {
trailingEmptyLines++;
}
else {
break;
}
}
if (trailingEmptyLines >= PADDING_LINES) {
return content;
}
const linesToAdd = PADDING_LINES - trailingEmptyLines;
return (content + '\n'.repeat(linesToAdd));
}
// TODO: [🧠] Maybe export
/**
* Removes Markdown (or HTML) comments
*
* @param {string} content - The string to remove comments from.
* @returns {string} The input string with all comments removed.
*
* @public exported from `@promptbook/markdown-utils`
*/
function removeMarkdownComments(content) {
return spacetrim.spaceTrim(content.replace(/<!--(.*?)-->/gs, ''));
}
/**
* Utility to determine if a pipeline string is in flat format.
* A flat pipeline is a simple text without proper structure (headers, blocks, etc).
*
* @public exported from `@promptbook/editable`
*/
function isFlatPipeline(pipelineString) {
pipelineString = removeMarkdownComments(pipelineString);
pipelineString = spacetrim.spaceTrim(pipelineString);
const isMarkdownBeginningWithHeadline = pipelineString.startsWith('# ');
//const isLastLineReturnStatement = pipelineString.split(/\r?\n/).pop()!.split('`').join('').startsWith('->');
const isBacktickBlockUsed = pipelineString.includes('```');
const isQuoteBlocksUsed = /^>\s+/m.test(pipelineString);
const isBlocksUsed = isBacktickBlockUsed || isQuoteBlocksUsed;
// TODO: [🧉] Also (double)check
// > const usedCommands
// > const isBlocksUsed
// > const returnStatementCount
const isFlat = !isMarkdownBeginningWithHeadline && !isBlocksUsed; /* && isLastLineReturnStatement */
return isFlat;
}
/**
* Converts a pipeline structure to its string representation.
*
* Transforms a flat, simple pipeline into a properly formatted pipeline string
* with sections for title, prompt, and return statement.
*
* @public exported from `@promptbook/editable`
*/
function deflatePipeline(pipelineString) {
if (!isFlatPipeline(pipelineString)) {
return pipelineString;
}
pipelineString = spacetrim.spaceTrim(pipelineString);
const pipelineStringLines = pipelineString.split(/\r?\n/);
const potentialReturnStatement = pipelineStringLines.pop();
let returnStatement;
if (/(-|=)>\s*\{.*\}/.test(potentialReturnStatement)) {
// Note: Last line is return statement
returnStatement = potentialReturnStatement;
}
else {
// Note: Last line is not a return statement
returnStatement = `-> {${DEFAULT_BOOK_OUTPUT_PARAMETER_NAME}}`;
pipelineStringLines.push(potentialReturnStatement);
}
const prompt = spacetrim.spaceTrim(pipelineStringLines.join('\n'));
let quotedPrompt;
if (prompt.split(/\r?\n/).length <= 1) {
quotedPrompt = `> ${prompt}`;
}
else {
quotedPrompt = spacetrim.spaceTrim((block) => `
\`\`\`
${block(prompt.split('`').join('\\`'))}
\`\`\`
`);
}
pipelineString = validatePipelineString(spacetrim.spaceTrim((block) => `
# ${DEFAULT_BOOK_TITLE}
## Prompt
${block(quotedPrompt)}
${returnStatement}
`));
// <- TODO: Maybe use book` notation
return padBook(pipelineString);
}
// TODO: Unit test
/**
* Normalizes the markdown by flattening the structure
*
* - It always have h1 - if there is no h1 in the markdown, it will be added `DEFAULT_BOOK_TITLE`
* - All other headings are normalized to h2
*
* @public exported from `@promptbook/markdown-utils`
*/
function flattenMarkdown(markdown) {
const sections = splitMarkdownIntoSections(markdown);
if (sections.length === 0) {
return `# ${DEFAULT_BOOK_TITLE}`;
}
let flattenedMarkdown = '';
const parsedSections = sections.map(parseMarkdownSection);
const firstSection = parsedSections.shift();
if (firstSection.level === 1) {
flattenedMarkdown += `# ${firstSection.title}` + `\n\n`;
flattenedMarkdown += firstSection.content + `\n\n`; // <- [🧠] Maybe 3 new lines?
}
else {
parsedSections.unshift(firstSection);
flattenedMarkdown += `# ${DEFAULT_BOOK_TITLE}` + `\n\n`; // <- [🧠] Maybe 3 new lines?
}
for (const { title, content } of parsedSections) {
flattenedMarkdown += `## ${title}` + `\n\n`;
flattenedMarkdown += content + `\n\n`; // <- [🧠] Maybe 3 new lines?
}
return spacetrim.spaceTrim(flattenedMarkdown);
}
/**
* TODO: [🏛] This can be part of markdown builder
* Note: [🕞] In past (commit 42086e1603cbed506482997c00a8ee979af0a247) there was much more
* sophisticated implementation of this function through parsing markdown into JSON structure
* and flattening the actual structure
* NOW we are working just with markdown string and its good enough
*/
/**
* Normalizes inline parameter mentions wrapped in code spans before markdown flattening.
*
* @private internal utility of `preparePipelineString`
*/
const INLINE_CODE_PARAMETER_REGEXP = /`\{(?<parameterName>[a-z0-9_]+)\}`/gi;
/**
* Normalizes inline return statements wrapped in code spans before markdown flattening.
*
* @private internal utility of `preparePipelineString`
*/
const INLINE_CODE_RETURN_PARAMETER_REGEXP = /`->\s+\{(?<parameterName>[a-z0-9_]+)\}`/gi;
/**
* Removes shebang/comments and normalizes markdown into a parseable pipeline form.
*
* @private internal utility of `parsePipeline`
*/
function preparePipelineString(pipelineString, $pipelineJson) {
pipelineString = removePipelineShebang(pipelineString, $pipelineJson);
pipelineString = removeMarkdownComments(pipelineString);
pipelineString = spacetrim.spaceTrim(pipelineString);
// <- TODO: [😧] `spaceTrim` should preserve discriminated type *(or at lease `PipelineString`)*
pipelineString = deflatePipeline(pipelineString);
pipelineString = flattenMarkdown(pipelineString);
pipelineString = pipelineString.replaceAll(INLINE_CODE_PARAMETER_REGEXP, '{$<parameterName>}');
pipelineString = pipelineString.replaceAll(INLINE_CODE_RETURN_PARAMETER_REGEXP, '-> {$<parameterName>}');
return pipelineString;
}
/**
* Validates and removes the optional `#!` shebang line for `.book` files.
*
* @private internal utility of `preparePipelineString`
*/
function removePipelineShebang(pipelineString, $pipelineJson) {
if (!pipelineString.startsWith('#!')) {
return pipelineString;
}
const [shebangLine, ...restLines] = pipelineString.split(/\r?\n/);
const isBookShebang = (shebangLine || '').includes('ptbk');
if (!isBookShebang) {
throw new ParseError(spacetrim.spaceTrim((block) => `
It seems that you try to parse a book file which has non-standard shebang line for book files:
Shebang line must contain 'ptbk'
You have:
${block(shebangLine || '(empty line)')}
It should look like this:
#!/usr/bin/env ptbk
${block(getPipelineIdentification($pipelineJson))}
`));
}
return validatePipelineString(restLines.join('\n'));
}
/**
* Supported script languages
*
* @private internal base for `ScriptLanguage`
*/
const SUPPORTED_SCRIPT_LANGUAGES = ['javascript', 'typescript', 'python'];
// <- TODO: [🏥] DRY
/**
* Makes first letter of a string uppercase
*
* Note: [🔂] This function is idempotent.
*
* @public exported from `@promptbook/utils`
*/
function capitalize(word) {
return word.substring(0, 1).toUpperCase() + word.substring(1);
}
/**
* Extracts all code blocks from markdown.
*
* Note: There are multiple similar functions:
* - `extractBlock` just extracts the content of the code block which is also used as built-in function for postprocessing
* - `extractJsonBlock` extracts exactly one valid JSON code block
* - `extractOneBlockFromMarkdown` extracts exactly one code block with language of the code block
* - `extractAllBlocksFromMarkdown` extracts all code blocks with language of the code block
*
* @param markdown any valid markdown
* @returns code blocks with language and content
* @throws {ParseError} if block is not closed properly
*
* @public exported from `@promptbook/markdown-utils`
*/
function extractAllBlocksFromMarkdown(markdown) {
const codeBlocks = [];
const lines = markdown.split(/\r?\n/);
// Note: [0] Ensure that the last block notated by gt > will be closed
lines.push('');
let currentCodeBlock = null;
for (const line of lines) {
if (line.startsWith('> ') || line === '>') {
if (currentCodeBlock === null) {
currentCodeBlock = { blockNotation: '>', language: null, content: '' };
} /* not else */
if (currentCodeBlock.blockNotation === '>') {
if (currentCodeBlock.content !== '') {
currentCodeBlock.content += '\n';
}
currentCodeBlock.content += line.slice(2);
}
}
else if (currentCodeBlock !== null && currentCodeBlock.blockNotation === '>' /* <- Note: [0] */) {
codeBlocks.push(currentCodeBlock);
currentCodeBlock = null;
}
/* not else */
if (line.startsWith('```')) {
const language = line.slice(3).trim() || null;
if (currentCodeBlock === null) {
currentCodeBlock = { blockNotation: '```', language, content: '' };
}
else {
if (language !== null) {
throw new ParseError(`${capitalize(currentCodeBlock.language || 'the')} code block was not closed and already opening new ${language} code block`);
}
codeBlocks.push(currentCodeBlock);
currentCodeBlock = null;
}
}
else if (currentCodeBlock !== null && currentCodeBlock.blockNotation === '```') {
if (currentCodeBlock.content !== '') {
currentCodeBlock.content += '\n';
}
currentCodeBlock.content += line.split('\\`\\`\\`').join('```') /* <- TODO: Maybe make proper unescape */;
}
}
if (currentCodeBlock !== null) {
throw new ParseError(`${capitalize(currentCodeBlock.language || 'the')} code block was not closed at the end of the markdown`);
}
return codeBlocks;
}
// TODO: Maybe name for `blockNotation` instead of '```' and '>'
/**
* Extracts exactly ONE code block from markdown.
*
* - When there are multiple or no code blocks the function throws a `ParseError`
*
* Note: There are multiple similar functions:
* - `extractBlock` just extracts the content of the code block which is also used as built-in function for postprocessing
* - `extractJsonBlock` extracts exactly one valid JSON code block
* - `extractOneBlockFromMarkdown` extracts exactly one code block with language of the code block
* - `extractAllBlocksFromMarkdown` extracts all code blocks with language of the code block
*
* @param markdown any valid markdown
* @returns code block with language and content
* @throws {ParseError} if there is not exactly one code block in the markdown
*
* @public exported from `@promptbook/markdown-utils`
*/
function extractOneBlockFromMarkdown(markdown) {
const codeBlocks = extractAllBlocksFromMarkdown(markdown);
if (codeBlocks.length !== 1) {
throw new ParseError(spacetrim.spaceTrim((block) => `
There should be exactly 1 code block in task section, found ${codeBlocks.length} code blocks
${block(codeBlocks.map((block, i) => `Block ${i + 1}:\n${block.content}`).join('\n\n\n'))}
`));
}
return codeBlocks[0];
}
// TODO: [🍓][🌻] Decide of this is internal utility, external util OR validator/postprocessor
/**
* Parses the given script and returns the list of all used variables that are not defined in the script
*
* @param script from which to extract the variables
* @returns the list of variable names
* @throws {ParseError} if the script is invalid
*
* @public exported from `@promptbook/javascript`
*/
function extractVariablesFromJavascript(script) {
const variables = new Set();
const originalScript = script;
script = `(()=>{${script}})()`;
try {
for (let i = 0; i < LOOP_LIMIT; i++)
try {
eval(script); // <- TODO: Use `JavascriptExecutionTools.execute` here
}
catch (error) {
if (!(error instanceof ReferenceError)) {
throw error;
}
/*
Note: Parsing the error
🌟 Most devices:
[PipelineUrlError: thing is not defined]
🍏 iPhone`s Safari:
[PipelineUrlError: Can't find variable: thing]
*/
let variableName = undefined;
if (error.message.startsWith(`Can't`)) {
// 🍏 Case
variableName = error.message.split(' ').pop();
}
else {
// 🌟 Case
variableName = error.message.split(' ').shift();
}
if (variableName === undefined) {
throw error;
}
if (script.includes(variableName + '(')) {
script = `const ${variableName} = ()=>'';` + script;
}
else {
variables.add(variableName);
script = `const ${variableName} = '';` + script;
}
}
}
catch (error) {
assertsError(error);
throw new ParseError(spacetrim.spaceTrim((block) => `
Can not extract variables from the script
${block(error.stack || error.message)}
Found variables:
${Array.from(variables)
.map((variableName, i) => `${i + 1}) ${variableName}`)
.join('\n')}
The script:
\`\`\`javascript
${block(originalScript)}
\`\`\`
`));
}
return variables;
}
// TODO: [🔣] Support for multiple languages - python, java,...
/**
* Parses the task and returns the list of all parameter names
*
* @param template the string template with parameters in {curly} braces
* @returns the list of parameter names
*
* @public exported from `@promptbook/utils`
*/
function extractParameterNames(template) {
const matches = template.matchAll(/{\w+}/g);
const parameterNames = new Set();
for (const match of matches) {
const parameterName = match[0].slice(1, -1);
parameterNames.add(parameterName);
}
return parameterNames;
}
/**
* Parses the task and returns the set of all used parameters
*
* @param task the task with used parameters
* @returns the set of parameter names
* @throws {ParseError} if the script is invalid
*
* @public exported from `@promptbook/core` <- Note: [👖] This utility is so tightly interconnected with the Promptbook that it is not exported as util but in core
*/
function extractParameterNamesFromTask(task) {
const { title, description, taskType, content, preparedContent, jokerParameterNames, foreach } = task;
const parameterNames = new Set();
let contentParameters;
if (taskType !== 'SCRIPT_TASK') {
contentParameters = extractParameterNames(content);
}
else {
// TODO: What if script is not javascript?
// const { contentLanguage } = task;
// if (contentLanguage !== 'javascript') {
contentParameters = extractVariablesFromJavascript(content);
}
for (const parameterName of [
...extractParameterNames(title),
...extractParameterNames(description || ''),
...contentParameters,
...extractParameterNames(preparedContent || ''),
]) {
parameterNames.add(parameterName);
}
for (const jokerName of jokerParameterNames || []) {
parameterNames.add(jokerName);
}
parameterNames.delete('content');
// <- Note {websiteContent} is used in `preparedContent`
// Note: [🍭] Fixing dependent subparameterName from FOREACH command
if (foreach !== undefined) {
for (const subparameterName of foreach.inputSubparameterNames) {
if (parameterNames.has(subparameterName)) {
parameterNames.delete(subparameterName);
parameterNames.add(foreach.parameterName);
// <- TODO: [🏮] Warn/logic error when `subparameterName` not used
}
}
}
return parameterNames;
}
// TODO: [🔣] If script require contentLanguage
/**
* Matches the trailing return statement of a task section.
*
* @private internal utility of `processPipelineSection`
*/
const RESULTING_PARAMETER_LINE_REGEXP = /^->\s*\{(?<resultingParamName>[a-z0-9_]+)\}/im;
/**
* Parses, applies, and persists one h2 task section.
*
* @private internal utility of `parsePipeline`
*/
function processPipelineSection(pipelineSection, $pipelineJson, getUniqueSectionName) {
const { $taskJson, language } = createTaskJsonFromSection(pipelineSection, getUniqueSectionName);
const commands = parsePipelineTaskCommands(pipelineSection.content);
applyDefaultTaskSectionType($taskJson, commands, $pipelineJson);
for (const commandItem of commands) {
applyPipelineTaskCommand(commandItem, $taskJson, $pipelineJson);
}
applyScriptTaskLanguage($taskJson, language, $pipelineJson);
registerTaskDependentParameters($taskJson, $pipelineJson);
persistTaskIfNeeded($taskJson, $pipelineJson);
}
/**
* Creates the mutable task JSON shell from one markdown section.
*
* @private internal utility of `processPipelineSection`
*/
function createTaskJsonFromSection(pipelineSection, getUniqueSectionName) {
const { language, content } = extractOneBlockFromMarkdown(pipelineSection.content);
const normalizedLanguage = language || undefined;
const title = pipelineSection.title || DEFAULT_TASK_TITLE;
const $taskJson = {
isSectionTypeSet: false,
isTask: true,
taskType: undefined /* <- Note: [🍙] Putting here placeholder to keep `taskType` on top at final JSON */,
name: getUniqueSectionName(title),
title,
description: extractPipelineDescription(pipelineSection.content),
content,
// <- TODO: [🍙] Some standard order of properties
};
const resultingParameterName = extractResultingParameterName(pipelineSection.content);
if (resultingParameterName !== undefined) {
$taskJson.resultingParameterName = resultingParameterName;
}
return {
$taskJson,
language: normalizedLanguage,
};
}
/**
* Extracts the optional trailing `-> {parameter}` statement from a section body.
*
* @private internal utility of `processPipelineSection`
*/
function extractResultingParameterName(sectionContent) {
var _a;
const lastLine = sectionContent.split(/\r?\n/).pop();
const resultingParameterNameMatch = RESULTING_PARAMETER_LINE_REGEXP.exec(lastLine);
return (_a = resultingParameterNameMatch === null || resultingParameterNameMatch === void 0 ? void 0 : resultingParameterNameMatch.groups) === null || _a === void 0 ? void 0 : _a.resultingParamName;
}
/**
* Parses all list-item commands declared inside one task section.
*
* @private internal utility of `processPipelineSection`
*/
function parsePipelineTaskCommands(sectionContent) {
return extractAllListItemsFromMarkdown(sectionContent).map((listItem) => ({
listItem,
command: parseCommand(listItem, 'PIPELINE_TASK'),
}));
}
/**
* Applies the implicit default `PROMPT_TASK` section type when no SECTION command is present.
*
* @private internal utility of `processPipelineSection`
*/
function applyDefaultTaskSectionType($taskJson, commands, $pipelineJson) {
const isSectionCommandPresent = commands.some(({ command }) => command.type === 'SECTION');
if (isSectionCommandPresent) {
return;
}
sectionCommandParser.$applyToTaskJson({ type: 'SECTION', taskType: 'PROMPT_TASK' }, $taskJson, $pipelineJson);
}
/**
* Parses and applies one command declared inside a task section.
*
* @private internal utility of `processPipelineSection`
*/
function applyPipelineTaskCommand(commandItem, $taskJson, $pipelineJson) {
const { listItem, command } = commandItem;
const commandParser = getParserForCommand(command);
if (commandParser.isUsedInPipelineTask !== true /* <- Note: [🦦][4] */) {
throw new ParseError(spacetrim.spaceTrim((block) => `
Command \`${command.type}\` is not allowed in the task of the promptbook ONLY at the pipeline head
${block(getPipelineIdentification($pipelineJson))}
`)); // <- TODO: [🚞]
}
try {
commandParser.$applyToTaskJson(command, $taskJson, $pipelineJson);
// <- Note: [🦦] Its strange that this assertion must be here, [🦦][4] should do this assertion implicitly
}
catch (error) {
if (!(error instanceof ParseError)) {
throw error;
}
throw new ParseError(spacetrim.spaceTrim((block) => `
Command \`${command.type}\` failed to apply to the task
The error:
${block(error.message)}
Current state of the task:
${block(JSON.stringify($taskJson, null, 4))}
Command that failed to apply:
${block(JSON.stringify(command, null, 4))}
*<- Maybe wrong order of commands in section?*
Raw command:
- ${listItem}
Usage of ${command.type}:
${block(commandParser.examples.map((example) => `- ${example}`).join('\n'))}
${block(getPipelineIdentification($pipelineJson))}
`)); // <- TODO: [🚞]
}
if (command.type === 'PARAMETER') {
defineParameter($pipelineJson, command);
// <- Note: [🍣]
}
}
/**
* Validates and stores the language for SCRIPT tasks.
*
* @private internal utility of `processPipelineSection`
*/
function applyScriptTaskLanguage($taskJson, language, $pipelineJson) {
const isScriptTask = $taskJson.taskType === 'SCRIPT_TASK';
if (!isScriptTask) {
return;
}
if (!language) {
throw new ParseError(spacetrim.spaceTrim((block) => `
You must specify the language of the script in the \`SCRIPT\` task
${block(getPipelineIdentification($pipelineJson))}
`));
}
if (!SUPPORTED_SCRIPT_LANGUAGES.includes(language)) {
throw new ParseError(spacetrim.spaceTrim((block) => `
Script language ${language} is not supported.
Supported languages are:
${block(SUPPORTED_SCRIPT_LANGUAGES.join(', '))}
`));
}
$taskJson.contentLanguage = language;
}
/**
* Extracts task dependencies and ensures referenced parameters exist.
*
* @private internal utility of `processPipelineSection`
*/
function registerTaskDependentParameters($taskJson, $pipelineJson) {
$taskJson.dependentParameterNames = Array.from(extractParameterNamesFromTask($taskJson));
for (const parameterName of $taskJson.dependentParameterNames) {
// TODO: [🧠] This definition should be made first in the task
defineParameter($pipelineJson, {
parameterName,
parameterDescription: null,
isInput: false,
isOutput: false,
// <- Note: In this case null+false+false means that we do not know yet if it is input or output and we will set it later
});
}
}
/**
* Removes transient parsing flags and persists real tasks into the pipeline JSON.
*
* @private internal utility of `processPipelineSection`
*/
function persistTaskIfNeeded($taskJson, $pipelineJson) {
/*
// TODO: [🍧] This should be checked in `MODEL` command + better error message
if ($taskJson.taskType !== 'PROMPT_TASK' && $taskJson.modelRequirements !== undefined) {
throw new UnexpectedError(
spaceTrim(
(block) => `
Model requirements are defined for the block type ${
$taskJson.taskType
} which is not a \`PROMPT\` task
This should be avoided by the \`modelCommandParser\`
${block(getPipelineIdentification($pipelineJson))}
`,
),
);
}
*/
if (!$taskJson.isTask) {
return;
}
delete $taskJson.isSectionTypeSet;
delete $taskJson.isTask;
// TODO: [🍙] Maybe do reorder of `$taskJson` here
$pipelineJson.tasks.push($taskJson);
}
/**
* Compile pipeline from string (markdown) format to JSON format synchronously
*
* Note: There are 3 similar functions:
* - `compilePipeline` **(preferred)** - which properly compiles the promptbook and uses embedding for external knowledge
* - `parsePipeline` - use only if you need to compile promptbook synchronously and it contains NO external knowledge
* - `preparePipeline` - just one step in the compilation process
*
* Note: This function does not validate logic of the pipeline only the parsing
* Note: This function acts as compilation process
*
* @param pipelineString {Promptbook} in string markdown format (.book.md)
* @returns {Promptbook} compiled in JSON format (.bookc)
* @throws {ParseError} if the promptbook string is not valid
*
* @public exported from `@promptbook/core`
*/
function parsePipeline(pipelineString) {
const $pipelineJson = createInitialPipelineJson(pipelineString);
const preparedPipelineString = preparePipelineString(pipelineString, $pipelineJson);
const { pipelineHead, pipelineSections } = parsePreparedPipelineSections(preparedPipelineString, $pipelineJson);
const getUniqueSectionName = createUniqueSectionNameResolver(pipelineSections);
applyPipelineHead(pipelineHead, $pipelineJson);
for (const pipelineSection of pipelineSections) {
processPipelineSection(pipelineSection, $pipelineJson, getUniqueSectionName);
}
return finalizeParsedPipeline($pipelineJson);
}
// TODO: [🧠] Maybe more things here can be refactored as high-level abstractions
// TODO: [main] !!4 Warn if used only sync version
// TODO: [🚞] Report here line/column of error
/**
* Compile pipeline from string (markdown) format to JSON format
*
* Note: This function does not validate logic of the pipeline only the parsing
* Note: This function acts as compilation process
*
* @see https://github.com/webgptorg/promptbook/discussions/196
* @param pipelineString {Promptbook} in string markdown format (.book.md)
* @param options - Configuration of the remote server
* @returns {Promptbook} compiled in JSON format (.bookc)
* @throws {ParseError} if the promptbook string is not valid
*
* @public exported from `@promptbook/remote-client`
*/
async function compilePipelineOnRemoteServer(pipelineString, options) {
let pipelineJson = parsePipeline(pipelineString);
pipelineJson = await preparePipelineOnRemoteServer(pipelineJson, options);
// Note: No need to use `$exportJson` because `parsePipeline` and `preparePipeline` already do that
return pipelineJson;
}
// TODO: [🐚] Do not return `Promise<PipelineJson>` But `PreparationTask`
/**
* Profile for Remote provider
*/
const REMOTE_PROVIDER_PROFILE = {
name: 'REMOTE',
fullname: 'Remote Server',
color: '#6b7280',
};
/**
* 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}`;
}
get profile() {
return REMOTE_PROVIDER_PROFILE;
}
/**
* 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: [🕴] Deprecate pipeline server and all of its components
// 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`
exports.BOOK_LANGUAGE_VERSION = BOOK_LANGUAGE_VERSION;
exports.PROMPTBOOK_ENGINE_VERSION = PROMPTBOOK_ENGINE_VERSION;
exports.RemoteLlmExecutionTools = RemoteLlmExecutionTools;
exports.compilePipelineOnRemoteServer = compilePipelineOnRemoteServer;
exports.preparePipelineOnRemoteServer = preparePipelineOnRemoteServer;
Object.defineProperty(exports, '__esModule', { value: true });
}));
//# sourceMappingURL=index.umd.js.map