@promptbook/openai
Version:
Promptbook: Turn your company's scattered knowledge into AI ready books
5,550 lines โข 218 kB
JavaScript
import colors from 'colors';
import spaceTrim$2, { spaceTrim as spaceTrim$1 } from 'spacetrim';
import 'path';
import { randomBytes } from 'crypto';
import 'crypto-js';
import 'crypto-js/enc-hex';
import Bottleneck from 'bottleneck';
import OpenAI from 'openai';
import { io } from 'socket.io-client';
// โ ๏ธ 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.105.0-23';
/**
* TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
* Note: [๐] Ignore a discrepancy between file name and entity name
*/
/**
* Detects if the code is running in a browser environment in main thread (Not in a web worker)
*
* Note: `$` is used to indicate that this function is not a pure function - it looks at the global object to determine the environment
*
* @public exported from `@promptbook/utils`
*/
function $isRunningInBrowser() {
try {
return typeof window !== 'undefined' && typeof window.document !== 'undefined';
}
catch (e) {
return false;
}
}
/**
* TODO: [๐บ]
*/
/**
* Detects if the code is running in a web worker
*
* Note: `$` is used to indicate that this function is not a pure function - it looks at the global object to determine the environment
*
* @public exported from `@promptbook/utils`
*/
function $isRunningInWebWorker() {
try {
// Note: Check for importScripts which is specific to workers
// and not available in the main browser thread
return (typeof self !== 'undefined' &&
typeof self.importScripts === 'function');
}
catch (e) {
return false;
}
}
/**
* TODO: [๐บ]
*/
/**
* 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
*
* @public exported from `@promptbook/utils`
* @see https://github.com/hejny/spacetrim#usage
*/
const spaceTrim = spaceTrim$1;
/**
* @private util of `@promptbook/color`
* @de
*/
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.
*
* @private util of `@promptbook/color`
* @deprecated [๐คก] Use some better functional library instead of `TakeChain`
*/
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;
}
},
});
}
/**
* ๐จ 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
*/
/**
* 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}`);
}
}
/**
* 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 {
/**
* 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) {
if (CSS_COLORS[color]) {
return Color.fromString(CSS_COLORS[color]);
// -----
}
else if (Color.isHexColorString(color)) {
return Color.fromHex(color);
// -----
}
else if (/^hsl\(\s*(\d+)\s*,\s*(\d+(?:\.\d+)?%)\s*,\s*(\d+(?:\.\d+)?%)\)$/.test(color)) {
return Color.fromHsl(color);
// -----
}
else if (/^rgb\((\s*[0-9-.%]+\s*,?){3}\)$/.test(color)) {
// TODO: [0] Should be fromRgbString and fromRgbaString one or two functions
return Color.fromRgbString(color);
// -----
}
else if (/^rgba\((\s*[0-9-.%]+\s*,?){4}\)$/.test(color)) {
return Color.fromRgbaString(color);
// -----
}
else {
throw new Error(`Can not create a new Color instance from string "${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) {
const hexOriginal = hex;
if (hex.startsWith('#')) {
hex = hex.substring(1);
}
if (hex.length === 3) {
return Color.fromHex3(hex);
}
if (hex.length === 4) {
return Color.fromHex4(hex);
}
if (hex.length === 6) {
return Color.fromHex6(hex);
}
if (hex.length === 8) {
return Color.fromHex8(hex);
}
throw new Error(`Can not parse color from hex string "${hexOriginal}"`);
}
/**
* Creates a new Color instance from color in hex format with 3 color digits (without alpha channel)
*
* @param color in hex for example `09d`
* @returns Color object
*/
static fromHex3(hex) {
const r = parseInt(hex.substr(0, 1), 16) * 16;
const g = parseInt(hex.substr(1, 1), 16) * 16;
const b = parseInt(hex.substr(2, 1), 16) * 16;
return take(new Color(r, g, b));
}
/**
* Creates a new Color instance from color in hex format with 4 digits (with alpha channel)
*
* @param color in hex for example `09df`
* @returns Color object
*/
static fromHex4(hex) {
const r = parseInt(hex.substr(0, 1), 16) * 16;
const g = parseInt(hex.substr(1, 1), 16) * 16;
const b = parseInt(hex.substr(2, 1), 16) * 16;
const a = parseInt(hex.substr(3, 1), 16) * 16;
return take(new Color(r, g, b, a));
}
/**
* Creates a new Color instance from color in hex format with 6 color digits (without alpha channel)
*
* @param color in hex for example `009edd`
* @returns Color object
*/
static fromHex6(hex) {
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
return take(new Color(r, g, b));
}
/**
* Creates a new Color instance from color in hex format with 8 color digits (with alpha channel)
*
* @param color in hex for example `009edd`
* @returns Color object
*/
static fromHex8(hex) {
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
const a = parseInt(hex.substr(6, 2), 16);
return take(new Color(r, g, b, a));
}
/**
* 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) {
const match = hsl.match(/^hsl\(\s*([0-9.]+)\s*,\s*([0-9.]+)%\s*,\s*([0-9.]+)%\s*\)$/);
if (!match) {
throw new Error(`Invalid hsl string format: "${hsl}"`);
}
const h = parseFloat(match[1]);
const s = parseFloat(match[2]) / 100;
const l = parseFloat(match[3]) / 100;
// HSL to RGB conversion
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, g1 = 0, b1 = 0;
if (h >= 0 && h < 60) {
r1 = c;
g1 = x;
b1 = 0;
}
else if (h >= 60 && h < 120) {
r1 = x;
g1 = c;
b1 = 0;
}
else if (h >= 120 && h < 180) {
r1 = 0;
g1 = c;
b1 = x;
}
else if (h >= 180 && h < 240) {
r1 = 0;
g1 = x;
b1 = c;
}
else if (h >= 240 && h < 300) {
r1 = x;
g1 = 0;
b1 = c;
}
else if (h >= 300 && h < 360) {
r1 = c;
g1 = 0;
b1 = x;
}
const r = Math.round((r1 + m) * 255);
const g = Math.round((g1 + m) * 255);
const b = Math.round((b1 + m) * 255);
return take(new Color(r, g, b));
}
/**
* 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) {
const match = rgb.match(/^rgb\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/);
if (!match) {
throw new Error(`Invalid rgb string format: "${rgb}"`);
}
const parseChannel = (value) => {
if (value.endsWith('%')) {
// Percentage value
const percent = parseFloat(value);
return Math.round((percent / 100) * 255);
}
else {
// Numeric value
return Math.round(parseFloat(value));
}
};
const r = parseChannel(match[1]);
const g = parseChannel(match[2]);
const b = parseChannel(match[3]);
return take(new Color(r, g, b));
}
/**
* 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) {
const match = rgba.match(/^rgba\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/);
if (!match) {
throw new Error(`Invalid rgba string format: "${rgba}"`);
}
const parseChannel = (value) => {
if (value.endsWith('%')) {
const percent = parseFloat(value);
return Math.round((percent / 100) * 255);
}
else {
return Math.round(parseFloat(value));
}
};
const parseAlpha = (value) => {
if (value.endsWith('%')) {
const percent = parseFloat(value);
return Math.round((percent / 100) * 255);
}
else {
const alphaFloat = parseFloat(value);
// If alpha is between 0 and 1, treat as float
if (alphaFloat <= 1) {
return Math.round(alphaFloat * 255);
}
// Otherwise, treat as 0-255
return Math.round(alphaFloat);
}
};
const r = parseChannel(match[1]);
const g = parseChannel(match[2]);
const b = parseChannel(match[3]);
const a = parseAlpha(match[4]);
return take(new Color(r, g, b, a));
}
/**
* 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 take(new Color(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 (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));
}
/**
* 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) {
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(new Color(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`);
}
}
/**
* TODO: [๐ฅป] Split Color class and color type
* TODO: For each method a corresponding static method should be created
* Like clone can be done by color.clone() OR Color.clone(color)
* TODO: Probably as an independent LIB OR add to LIB xyzt (ask @roseckyj)
* TODO: !! Transfer back to Collboard (whole directory)
* TODO: Maybe [๐๏ธโโ๏ธ] change ACRY toString => (toHex) toRgb when there will be toRgb and toRgba united
* TODO: Convert getters to methods - getters only for values
* TODO: Write tests
* TODO: Getters for alpha, opacity, transparency, r, b, g, h, s, l, a,...
* TODO: [0] Should be fromRgbString and fromRgbaString one or two functions + one or two regex
* TODO: Use rgb, rgba, hsl for testing and parsing with the same regex
* TODO: Regex for rgb, rgba, hsl does not support all options like deg, rad, turn,...
* TODO: Convolution matrix
* TODO: Maybe connect with textures
*/
/**
* 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
*/
/**
* 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'),
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: [๐ง ] Better system for generator warnings - not always "code" and "by `@promptbook/cli`"
/**
* The maximum number of iterations for a loops
*
* @private within the repository - too low-level in comparison with other `MAX_...`
*/
const LOOP_LIMIT = 1000;
/**
* Strings to represent various values in the context of parameter values
*
* @public exported from `@promptbook/utils`
*/
const VALUE_STRINGS = {
empty: '(nothing; empty string)',
null: '(no value; null)',
undefined: '(unknown value; undefined)',
nan: '(not a number; NaN)',
infinity: '(infinity; โ)',
negativeInfinity: '(negative infinity; -โ)',
unserializable: '(unserializable value)',
circular: '(circular JSON)',
};
/**
* Small number limit
*
* @public exported from `@promptbook/utils`
*/
const SMALL_NUMBER = 0.001;
/**
* Timeout for the connections in milliseconds
*
* @private within the repository - too low-level in comparison with other `MAX_...`
*/
const CONNECTION_TIMEOUT_MS = 7 * 1000;
// <- TODO: [โณ] Standardize timeouts, Make DEFAULT_TIMEOUT_MS as global constant
/**
* How many times to retry the connections
*
* @private within the repository - too low-level in comparison with other `MAX_...`
*/
const CONNECTION_RETRIES_LIMIT = 5;
// <- TODO: [๐งโโ๏ธ]
/**
* Default settings for parsing and generating CSV files in Promptbook.
*
* @public exported from `@promptbook/core`
*/
Object.freeze({
delimiter: ',',
quoteChar: '"',
newline: '\n',
skipEmptyLines: true,
});
/**
* Default rate limits (requests per minute)
*
* Note: Adjust based on the provider tier you are have
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_MAX_REQUESTS_PER_MINUTE = 60;
/**
* API request timeout in milliseconds
* Can be overridden via API_REQUEST_TIMEOUT environment variable
*
* @public exported from `@promptbook/core`
*/
const API_REQUEST_TIMEOUT = 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
*/
/**
* Orders JSON object by keys
*
* @returns The same type of object as the input re-ordered
* @public exported from `@promptbook/utils`
*/
function orderJson(options) {
const { value, order } = options;
const orderedValue = {
...(order === undefined ? {} : Object.fromEntries(order.map((key) => [key, undefined]))),
...value,
};
return orderedValue;
}
/**
* Freezes the given object and all its nested objects recursively
*
* Note: `$` is used to indicate that this function is not a pure function - it mutates given object
* Note: This function mutates the object and returns the original (but mutated-deep-freezed) object
*
* @returns The same object as the input, but deeply frozen
* @public exported from `@promptbook/utils`
*/
function $deepFreeze(objectValue) {
if (Array.isArray(objectValue)) {
return Object.freeze(objectValue.map((item) => $deepFreeze(item)));
}
const propertyNames = Object.getOwnPropertyNames(objectValue);
for (const propertyName of propertyNames) {
const value = objectValue[propertyName];
if (value && typeof value === 'object') {
$deepFreeze(value);
}
}
Object.freeze(objectValue);
return objectValue;
}
/**
* TODO: [๐ง ] Is there a way how to meaningfully test this utility
*/
/**
* Make error report URL for the given error
*
* @private private within the repository
*/
function getErrorReportUrl(error) {
const report = {
title: `๐ Error report from ${NAME}`,
body: spaceTrim$2((block) => `
\`${error.name || 'Error'}\` has occurred in the [${NAME}], please look into it @${ADMIN_GITHUB_NAME}.
\`\`\`
${block(error.message || '(no error message)')}
\`\`\`
## More info:
- **Promptbook engine version:** ${PROMPTBOOK_ENGINE_VERSION}
- **Book language version:** ${BOOK_LANGUAGE_VERSION}
- **Time:** ${new Date().toISOString()}
<details>
<summary>Stack trace:</summary>
## Stack trace:
\`\`\`stacktrace
${block(error.stack || '(empty)')}
\`\`\`
</details>
`),
};
const reportUrl = new URL(`https://github.com/webgptorg/promptbook/issues/new`);
reportUrl.searchParams.set('labels', 'bug');
reportUrl.searchParams.set('assignees', ADMIN_GITHUB_NAME);
reportUrl.searchParams.set('title', report.title);
reportUrl.searchParams.set('body', report.body);
return reportUrl;
}
/**
* This error type indicates that the error should not happen and its last check before crashing with some other error
*
* @public exported from `@promptbook/core`
*/
class UnexpectedError extends Error {
constructor(message) {
super(spaceTrim$1((block) => `
${block(message)}
Note: This error should not happen.
It's probably a bug in the pipeline collection
Please report issue:
${block(getErrorReportUrl(new Error(message)).href)}
Or contact us on ${ADMIN_EMAIL}
`));
this.name = 'UnexpectedError';
Object.setPrototypeOf(this, UnexpectedError.prototype);
}
}
/**
* This error type indicates that somewhere in the code non-Error object was thrown and it was wrapped into the `WrappedError`
*
* @public exported from `@promptbook/core`
*/
class WrappedError extends Error {
constructor(whatWasThrown) {
const tag = `[๐คฎ]`;
console.error(tag, whatWasThrown);
super(spaceTrim$1(`
Non-Error object was thrown
Note: Look for ${tag} in the console for more details
Please report issue on ${ADMIN_EMAIL}
`));
this.name = 'WrappedError';
Object.setPrototypeOf(this, WrappedError.prototype);
}
}
/**
* Helper used in catch blocks to assert that the error is an instance of `Error`
*
* @param whatWasThrown Any object that was thrown
* @returns Nothing if the error is an instance of `Error`
* @throws `WrappedError` or `UnexpectedError` if the error is not standard
*
* @private within the repository
*/
function assertsError(whatWasThrown) {
// Case 1: Handle error which was rethrown as `WrappedError`
if (whatWasThrown instanceof WrappedError) {
const wrappedError = whatWasThrown;
throw wrappedError;
}
// Case 2: Handle unexpected errors
if (whatWasThrown instanceof UnexpectedError) {
const unexpectedError = whatWasThrown;
throw unexpectedError;
}
// Case 3: Handle standard errors - keep them up to consumer
if (whatWasThrown instanceof Error) {
return;
}
// Case 4: Handle non-standard errors - wrap them into `WrappedError` and throw
throw new WrappedError(whatWasThrown);
}
/**
* Checks if the value is [๐] serializable as JSON
* If not, throws an UnexpectedError with a rich error message and tracking
*
* - Almost all primitives are serializable BUT:
* - `undefined` is not serializable
* - `NaN` is not serializable
* - Objects and arrays are serializable if all their properties are serializable
* - Functions are not serializable
* - Circular references are not serializable
* - `Date` objects are not serializable
* - `Map` and `Set` objects are not serializable
* - `RegExp` objects are not serializable
* - `Error` objects are not serializable
* - `Symbol` objects are not serializable
* - And much more...
*
* @throws UnexpectedError if the value is not serializable as JSON
* @public exported from `@promptbook/utils`
*/
function checkSerializableAsJson(options) {
const { value, name, message } = options;
if (value === undefined) {
throw new UnexpectedError(`${name} is undefined`);
}
else if (value === null) {
return;
}
else if (typeof value === 'boolean') {
return;
}
else if (typeof value === 'number' && !isNaN(value)) {
return;
}
else if (typeof value === 'string') {
return;
}
else if (typeof value === 'symbol') {
throw new UnexpectedError(`${name} is symbol`);
}
else if (typeof value === 'function') {
throw new UnexpectedError(`${name} is function`);
}
else if (typeof value === 'object' && Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
checkSerializableAsJson({ name: `${name}[${i}]`, value: value[i], message });
}
}
else if (typeof value === 'object') {
if (value instanceof Date) {
throw new UnexpectedError(spaceTrim$2((block) => `
\`${name}\` is Date
Use \`string_date_iso8601\` instead
Additional message for \`${name}\`:
${block(message || '(nothing)')}
`));
}
else if (value instanceof Map) {
throw new UnexpectedError(`${name} is Map`);
}
else if (value instanceof Set) {
throw new UnexpectedError(`${name} is Set`);
}
else if (value instanceof RegExp) {
throw new UnexpectedError(`${name} is RegExp`);
}
else if (value instanceof Error) {
throw new UnexpectedError(spaceTrim$2((block) => `
\`${name}\` is unserialized Error
Use function \`serializeError\`
Additional message for \`${name}\`:
${block(message || '(nothing)')}
`));
}
else {
for (const [subName, subValue] of Object.entries(value)) {
if (subValue === undefined) {
// Note: undefined in object is serializable - it is just omitted
continue;
}
checkSerializableAsJson({ name: `${name}.${subName}`, value: subValue, message });
}
try {
JSON.stringify(value); // <- TODO: [0]
}
catch (error) {
assertsError(error);
throw new UnexpectedError(spaceTrim$2((block) => `
\`${name}\` is not serializable
${block(error.stack || error.message)}
Additional message for \`${name}\`:
${block(message || '(nothing)')}
`));
}
/*
TODO: [0] Is there some more elegant way to check circular references?
const seen = new Set();
const stack = [{ value }];
while (stack.length > 0) {
const { value } = stack.pop()!;
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
throw new UnexpectedError(`${name} has circular reference`);
}
seen.add(value);
if (Array.isArray(value)) {
stack.push(...value.map((value) => ({ value })));
} else {
stack.push(...Object.values(value).map((value) => ({ value })));
}
}
}
*/
return;
}
}
else {
throw new UnexpectedError(spaceTrim$2((block) => `
\`${name}\` is unknown type
Additional message for \`${name}\`:
${block(message || '(nothing)')}
`));
}
}
/**
* TODO: Can be return type more type-safe? like `asserts options.value is JsonValue`
* TODO: [๐ง ][main] !!3 In-memory cache of same values to prevent multiple checks
* Note: [๐ ] This is how `checkSerializableAsJson` + `isSerializableAsJson` together can just retun true/false or rich error message
*/
/**
* Creates a deep clone of the given object
*
* Note: [๐] This 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
*/
/**
* Nonce which is used for replacing things in strings
*
* @private within the repository
*/
const REPLACING_NONCE = 'ptbkauk42kV2dzao34faw7FudQUHYPtW';
/**
* Nonce which is used as string which is not occurring in normal text
*
* @private within the repository
*/
const SALT_NONCE = 'ptbkghhewbvruets21t54et5';
/**
* Placeholder value indicating a parameter is missing its value.
*
* @private within the repository
*/
const RESERVED_PARAMETER_MISSING_VALUE = 'MISSING-' + REPLACING_NONCE;
/**
* Placeholder value indicating a parameter is restricted and cannot be used directly.
*
* @private within the repository
*/
const RESERVED_PARAMETER_RESTRICTED = 'RESTRICTED-' + REPLACING_NONCE;
/**
* The names of the parameters that are reserved for special purposes
*
* @public exported from `@promptbook/core`
*/
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
*/
/**
* 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;
}
}
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
*/
/**
* This error indicates problems parsing the format value
*
* For example, when the format value is not a valid JSON or CSV
* This is not thrown directly but in extended classes
*
* @public exported from `@promptbook/core`
*/
class AbstractFormatError extends Error {
// Note: To allow instanceof do not put here error `name`
// public readonly name = 'AbstractFormatError';
constructor(message) {
super(message);
Object.setPrototypeOf(this, AbstractFormatError.prototype);
}
}
/**
* This error indicates problem with parsing of CSV
*
* @public exported from `@promptbook/core`
*/
class CsvFormatError extends AbstractFormatError {
constructor(message) {
super(message);
this.name = 'CsvFormatError';
Object.setPrototypeOf(this, CsvFormatError.prototype);
}
}
/**
* AuthenticationError is thrown from login function which is dependency of remote server
*
* @public exported from `@promptbook/core`
*/
class AuthenticationError extends Error {
constructor(message) {
super(message);
this.name = 'AuthenticationError';
Object.setPrototypeOf(this, AuthenticationError.prototype);
}
}
/**
* This error indicates that the pipeline collection cannot be properly loaded
*
* @public exported from `@promptbook/core`
*/
class CollectionError extends Error {
constructor(message) {
super(message);
this.name = 'CollectionError';
Object.setPrototypeOf(this, CollectionError.prototype);
}
}
/**
* This error 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
*
* @public exported from `@promptbook/core`
* Note: Do not throw this error, its reserved for `checkExpectations` and `createPipelineExecutor` and public ONLY to be serializable through remote server
* Note: Always thrown in `checkExpectations` and catched in `createPipelineExecutor` and rethrown as `PipelineExecutionError`
* Note: This is a kindof subtype of PipelineExecutionError
*/
class ExpectError extends Error {
constructor(message) {
super(message);
this.name = 'ExpectError';
Object.setPrototypeOf(this, ExpectError.prototype);
}
}
/**
* This error indicates that the promptbook can not retrieve knowledge from external sources
*
* @public exported from `@promptbook/core`
*/
class KnowledgeScrapeError extends Error {
constructor(message) {
super(message);
this.name = 'KnowledgeScrapeError';
Object.setPrototypeOf(this, KnowledgeScrapeError.prototype);
}
}
/**
* This error type indicates that some limit was reached
*
* @public exported from `@promptbook/core`
*/
class LimitReachedError extends Error {
constructor(message) {
super(message);
this.name = 'LimitReachedError';
Object.setPrototypeOf(this, LimitReachedError.prototype);
}
}
/**
* This error type indicates that some tools are missing for pipeline execution or preparation
*
* @public exported from `@promptbook/core`
*/
class MissingToolsError extends Error {
constructor(message) {
super(spaceTrim$1((block) => `
${block(message)}
Note: You have probably forgot to provide some tools for pipeline execution or preparation
`));
this.name = 'MissingToolsError';
Object.setPrototypeOf(this, MissingToolsError.prototype);
}
}
/**
* This error indicates that promptbook 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$1((block) => `
${block(message)}
Note: This feature is not implemented yet but it will be soon.
If you want speed up the implementation or just read more, look here:
https://github.com/webgptorg/promptbook
Or contact us on pavol@ptbk.io
`));
this.name = 'NotYetImplementedError';
Object.setPrototypeOf(this, NotYetImplementedError.prototype);
}
}
/**
* 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)
*
* @private internal helper function
* @returns secure random token
*/
function $randomToken(randomness) {
return randomBytes(randomness).toString('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);
}
}
/**
* 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,
// 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$2((block) => `
${block(message)}
Original stack trace:
${block(stack || '')}
`);
}
const deserializedError = new ErrorClass(message);
deserializedError.id = id; // Assign id to the error object
return deserializedError;
}
/**
* Serializes an error into a [๐] JSON-serializable object
*
* @public exported from `@promptbook/utils`
*/
function serializeError(error) {
const { name, message, stack } = error;
const { id } = error;
if (!Object.keys(ALL_ERRORS).includes(name)) {
console.error(spaceTrim$2((block) => `
Cannot serialize error with name "${name}"
Authors of Promptbook probably forgot to add this error into the list of errors:
https://github.com/webgptorg/promptbook/blob/main/src/errors/0-index.ts
${block(stack || message)}
`));
}
return {
name: name,
message,
stack,
id, // Include id in the serialized object
};
}
/**
* Async version of Array.forEach
*
* @param array - Array to iterate over
* @param options - Options for the function
* @param callbackfunction - Function to call for each item
* @public exported from `@promptbook/utils`
* @deprecated [๐ช] Use queues instead
*/
async function forEachAsync(array, options, callbackfunction) {
const { maxParallelCount = Infinity } = options;
let index = 0;
let runningTasks = [];
const tasks = [];
for (const item of array) {
const currentIndex = index++;
const task = callbackfunction(item, currentIndex, array);
tasks.push(task);
runningTasks.push(task);
/* not await */ Promise.resolve(task).then(() => {
runningTasks = runningTasks.filter((runningTask) => runningTask !== task);
});
if (maxParallelCount < runningTasks.length) {
await Promise.race(runningTasks);
}
}
await Promise.all(tasks);
}
/**
* Format either small or big number
*
* @public exported from `@promptbook/utils`
*/
function numberToString(value) {
if (value === 0) {
return '0';
}
else if (Number.isNaN(value)) {
return VALUE_STRINGS.nan;
}
else if (value === Infinity) {
return VALUE_STRINGS.infinity;
}
else if (value === -Infinity) {
return VALUE_STRINGS.negativeInfinity;
}
for (let exponent = 0; exponent < 15; exponent++) {
const factor = 10 ** exponent;
const valueRounded = Math.round(value * factor) / factor;
if (Math.abs(value - valueRounded) / value < SMALL_NUMBER) {
return valueRounded.toFixed(exponent);
}
}
return value.toString();
}
/**
* Function `valueToString` will convert the given value to string
* This is useful and used in the `templateParameters` function
*
* Note: This function is not just calling `toString` method
* It's more complex and can handle this conversion specifically for LLM models
* See `VALUE_STRINGS`
*
* Note: There are 2 similar functions
* - `valueToString` converts value to string for LLM models as human-readable string
* - `asSerializable` converts value to string to preserve full information to be able to convert it back
*
* @public exported from `@promptbook/utils`
*/
function valueToString(value) {
try {
if (value === '') {
return VALUE_STRINGS.empty;
}
else if (value === null) {
return VALUE_STRINGS.null;
}
else if (value === undefined) {
return VALUE_STRINGS.undefined;
}
else if (typeof value === 'string') {
return value;
}
else if (typeof value === 'number') {
return numberToString(value);
}
else if (value instanceof Date) {
return value.toISOString();
}
else {
try {
return JSON.stringify(value);
}
catch (error) {
if (error instanceof TypeError && error.message.includes('circular structure')) {
return VALUE_STRINGS.circular;
}
throw error;
}
}
}
catch (error) {
assertsError(error);
console.error(error);
return VALUE_STRINGS.unserializable;
}
}
/**
* Replaces parameters in template with values from parameters object
*
* Note: This function is not places strings into string,
* It's more complex and can handle this operation specifically for LLM models
*
* @param template the template with parameters in {curly} braces
* @param parameters the object with parameters
* @returns the template with replaced parameters
* @throws {PipelineExecutionError} if parameter is not defined, not closed, or not opened
* @public exported from `@promptbook/utils`
*/
function templateParameters(template, parameters) {
for (const [parameterName, parameterValue] of Object.entries(parameters)) {
if (parameterValue === RESERVED_PARAMETER_MISSING_VALUE) {
throw new UnexpectedError(`Parameter \`{${parameterName}}\` has missing value`);
}
else if (parameterValue === RESERVED_PARAMETER_RESTRICTED) {
// TODO: [๐ต]
throw new UnexpectedError(`Parameter \`{${parameterName}}\` is restricted to use`);
}
}
let replacedTemplates = template;
let match;
let loopLimit = LOOP_LIMIT;
while ((match = /^(?<precol>.*){(?<parameterName>\w+)}(.*)/m /* <- Not global */
.exec(replacedTemplates))) {
if (loopLimit-- < 0) {
throw new LimitReachedError('Loop limit reached during parameters replacement in `templateParameters`');
}
const precol = match.groups.precol;
const parameterName = match.groups.parameterName;
if (parameterName === '') {
// Note: Skip empty placeholders. It's used to avoid confusion with JSON-like strings
continue;
}
if (parameterName.indexOf('{') !== -1 || parameterName.indexOf('}') !== -1) {
throw new PipelineExecutionError('Parameter is already opened or not closed');
}
if (parameters[parameterName] === undefined) {
throw new PipelineExecutionError(`Parameter \`{${parameterName}}\` is not defined`);
}
let parameterValue = parameters[parameterName];
if (parameterValue === undefined) {
throw new PipelineExecutionError(`Parameter \`{${parameterName}}\` is not defined`);
}
parameterValue = valueToString(parameterValue);
// Escape curly braces in parameter values to prevent prompt-injection
parameterValue = parameterValue.replace(/[{}]/g, '\\$&');
if (parameterValue.includes('\n') && /^\s*\W{0,3}\s*$/.test(precol)) {
parameterValue = parameterValue
.split('\n')
.map((line, index) => (index === 0 ? line : `${precol}${line}`))
.join('\n');
}
replacedTemplates =
replacedTemplates.substring(0, match.index + precol.length) +
parameterValue +
replacedTemplates.substring(match.index + precol.length + parameterName.length + 2);
}
// [๐ซ] Check if there are parameters that are not closed properly
if (/{\w+$/.test(replacedTemplates)) {
throw new PipelineExecutionError('Parameter is not closed');
}
// [๐ซ] Check if there are parameters that are not opened properly
if (/^\w+}/.test(replacedTemplates)) {
throw new PipelineExecutionError('Parameter is not opened');
}
return replacedTemplates;
}
/**
* Number of characters per standard line with 11pt Arial font size.
*
* @public exported from `@promptbook/utils`
*/
const CHARACTERS_PER_STANDARD_LINE = 63;
/**
* Number of lines per standard A4 page with 11pt Arial font size and standard margins and spacing.
*
* @public exported from `@promptbook/utils`
*/
const LINES_PER_STANDARD_PAGE = 44;
/**
* TODO: [๐ง ] Should be this `constants.ts` or `config.ts`?
* Note: [๐] Ignore a discrepancy between file name and entity name
*/
/**
* Counts number of characters in the text
*
* @public exported from `@promptbook/utils`
*/
function countCharacters(text) {
// Remove null characters
text = text.replace(/\0/g, '');
// 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, '-');
return text.length;
}
/**
* TODO: [๐ฅด] Implement counting in formats - like JSON, CSV, XML,...
* TODO: [๐ง ][โ๏ธ] Make some Promptbook-native token system
*/
/**
* Counts number of lines in the text
*
* Note: This does not check only for the presence of newlines, but also for the length of the standard line.
*
* @public exported from `@promptbook/utils`
*/
function countLines(text) {
if (text === '') {
return 0;
}
text = text.replace('\r\n', '\n');
text = text.replace('\r', '\n');
const lines = text.split('\n');
return lines.reduce((count, line) => count + Math.max(Math.ceil(line.length / CHARACTERS_PER_STANDARD_LINE), 1), 0);
}
/**
* TODO: [๐ฅด] Implement counting in formats - like JSON, CSV, XML,...
* TODO: [๐ง ][โ๏ธ] Make some Promptbook-native token system
*/
/**
* Counts number of pages in the text
*
* Note: This does not check only for the count of newlines, but also for the length of the standard line and length of the standard page.
*
* @public exported from `@promptbook/utils`
*/
function countPages(text) {
return Math.ceil(countLines(text) / LINES_PER_STANDARD_PAGE);
}
/**
* TODO: [๐ฅด] Implement counting in formats - like JSON, CSV, XML,...
* TODO: [๐ง ][โ๏ธ] Make some Promptbook-native token system
*/
/**
* Counts number of paragraphs in the text
*
* @public exported from `@promptbook/utils`
*/
function countParagraphs(text) {
return text.split(/\n\s*\n/).filter((paragraph) => paragraph.trim() !== '').length;
}
/**
* TODO: [๐ฅด] Implement counting in formats - like JSON, CSV, XML,...
* TODO: [๐ง ][โ๏ธ] Make some Promptbook-native token system
*/
/**
* Split text into sentences
*
* @public exported from `@promptbook/utils`
*/
function splitIntoSentences(text) {
return text.split(/[.!?]+/).filter((sentence) => sentence.trim() !== '');
}
/**
* Counts number of sentences in the text
*
* @public exported from `@promptbook/utils`
*/
function countSentences(text) {
return splitIntoSentences(text).length;
}
/**
* TODO: [๐ฅด] Implement counting in formats - like JSON, CSV, XML,...
* TODO: [๐ง ][โ๏ธ] Make some Promptbook-native token system
*/
/**
* Counts number of words in the text
*
* @public exported from `@promptbook/utils`
*/
function countWords(text) {
text = text.replace(/[\p{Extended_Pictographic}]/gu, 'a');
text = removeDiacritics(text);
// Add spaces before uppercase letters preceded by lowercase letters (for camelCase)
text = text.replace(/([a-z])([A-Z])/g, '$1 $2');
return text.split(/[^a-zะฐ-ั0-9]+/i).filter((word) => word.length > 0).length;
}
/**
* TODO: [๐ฅด] Implement counting in formats - like JSON, CSV, XML,...
* TODO: [๐ง ][โ๏ธ] Make some Promptbook-native token system
* TODO: [โ๏ธ] `countWords` should be just `splitWords(...).length`, and all other counters should use this pattern as well
*/
/**
* Simple wrapper `new Date().toISOString()`
*
* Note: `$` is used to indicate that this function is not a pure function - it is not deterministic because it depends on the current time
*
* @returns string_date branded type
* @public exported from `@promptbook/utils`
*/
function $getCurrentDate() {
return new Date().toISOString();
}
/**
* 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
*/
/**
* Normalizes a text string to snake_case format.
*
* Note: [๐] This function is idempotent.
*
* @param text The text string to be converted to snake_case format.
* @returns The normalized text in snake_case format.
* @example 'hello_world'
* @example 'i_love_promptbook'
* @public exported from `@promptbook/utils`
*/
function normalizeTo_snake_case(text) {
return normalizeTo_SCREAMING_CASE(text).toLowerCase();
}
/**
* Represents the uncertain value
*
* @public exported from `@promptbook/core`
*/
const ZERO_VALUE = $deepFreeze({ value: 0 });
/**
* Represents the uncertain value
*
* @public exported from `@promptbook/core`
*/
const UNCERTAIN_ZERO_VALUE = $deepFreeze({ value: 0, isUncertain: true });
/**
* Represents the usage with no resources consumed
*
* @public exported from `@promptbook/core`
*/
const ZERO_USAGE = $deepFreeze({
price: ZERO_VALUE,
input: {
tokensCount: ZERO_VALUE,
charactersCount: ZERO_VALUE,
wordsCount: ZERO_VALUE,
sentencesCount: ZERO_VALUE,
linesCount: ZERO_VALUE,
paragraphsCount: ZERO_VALUE,
pagesCount: ZERO_VALUE,
},
output: {
tokensCount: ZERO_VALUE,
charactersCount: ZERO_VALUE,
wordsCount: ZERO_VALUE,
sentencesCount: ZERO_VALUE,
linesCount: ZERO_VALUE,
paragraphsCount: ZERO_VALUE,
pagesCount: ZERO_VALUE,
},
});
/**
* Represents the usage with unknown resources consumed
*
* @public exported from `@promptbook/core`
*/
const UNCERTAIN_USAGE = $deepFreeze({
price: UNCERTAIN_ZERO_VALUE,
input: {
tokensCount: UNCERTAIN_ZERO_VALUE,
charactersCount: UNCERTAIN_ZERO_VALUE,
wordsCount: UNCERTAIN_ZERO_VALUE,
sentencesCount: UNCERTAIN_ZERO_VALUE,
linesCount: UNCERTAIN_ZERO_VALUE,
paragraphsCount: UNCERTAIN_ZERO_VALUE,
pagesCount: UNCERTAIN_ZERO_VALUE,
},
output: {
tokensCount: UNCERTAIN_ZERO_VALUE,
charactersCount: UNCERTAIN_ZERO_VALUE,
wordsCount: UNCERTAIN_ZERO_VALUE,
sentencesCount: UNCERTAIN_ZERO_VALUE,
linesCount: UNCERTAIN_ZERO_VALUE,
paragraphsCount: UNCERTAIN_ZERO_VALUE,
pagesCount: UNCERTAIN_ZERO_VALUE,
},
});
/**
* Note: [๐] Ignore a discrepancy between file name and entity name
*/
/**
* Helper of usage compute
*
* @param content the content of prompt or response
* @returns part of UsageCounts
*
* @private internal utility of LlmExecutionTools
*/
function computeUsageCounts(content) {
return {
charactersCount: { value: countCharacters(content) },
wordsCount: { value: countWords(content) },
sentencesCount: { value: countSentences(content) },
linesCount: { value: countLines(content) },
paragraphsCount: { value: countParagraphs(content) },
pagesCount: { value: countPages(content) },
};
}
/**
* Make UncertainNumber
*
* @param value value of the uncertain number, if `NaN` or `undefined`, it will be set to 0 and `isUncertain=true`
* @param isUncertain if `true`, the value is uncertain, otherwise depends on the value
*
* @private utility for initializating UncertainNumber
*/
function uncertainNumber(value, isUncertain) {
if (value === null || value === undefined || Number.isNaN(value)) {
return UNCERTAIN_ZERO_VALUE;
}
if (isUncertain === true) {
return { value, isUncertain };
}
return { value };
}
/**
* Create price per one token based on the string value found on openai page
*
* @private within the repository, used only as internal helper for `OPENAI_MODELS`
*/
function pricing(value) {
const [price, tokens] = value.split(' / ');
return parseFloat(price.replace('$', '')) / parseFloat(tokens.replace('M tokens', '')) / 1000000;
}
/**
* List of available OpenAI models with pricing
*
* Note: Synced with official API docs at 2025-11-19
*
* @see https://platform.openai.com/docs/models/
* @see https://openai.com/api/pricing/
* @public exported from `@promptbook/openai`
*/
const OPENAI_MODELS = exportJson({
name: 'OPENAI_MODELS',
value: [
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-5.1',
modelName: 'gpt-5.1',
modelDescription: 'The best model for coding and agentic tasks with configurable reasoning effort.',
pricing: {
prompt: pricing(`$1.25 / 1M tokens`),
output: pricing(`$10.00 / 1M tokens`),
},
},
{
modelVariant: 'CHAT',
modelTitle: 'gpt-5',
modelName: 'gpt-5',
modelDescription: "OpenAI's most advanced language model with unprecedented reasoning capabilities and 200K context window. Features revolutionary improvements in complex problem-solving, scientific reasoning, and creative tasks. Demonstrates human-level performance across diverse domains with enhanced safety measures and alignment. Represents the next generation of AI with superior understanding, nuanced responses, and advanced multimodal capabilities. DEPRECATED: Use gpt-5.1 instead.",
pricing: {
prompt: pricing(`$1.25 / 1M tokens`),
output: pricing(`$10.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-5-mini',
modelName: 'gpt-5-mini',
modelDescription: 'A faster, cost-efficient version of GPT-5 for well-defined tasks with 200K context window. Maintains core GPT-5 capabilities while offering 5x faster inference and significantly lower costs. Features enhanced instruction following and reduced latency for production applications requiring quick responses with high quality.',
pricing: {
prompt: pricing(`$0.25 / 1M tokens`),
output: pricing(`$2.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-5-nano',
modelName: 'gpt-5-nano',
modelDescription: 'The fastest, most cost-efficient version of GPT-5 with 200K context window. Optimized for summarization, classification, and simple reasoning tasks. Features 10x faster inference than base GPT-5 while maintaining good quality for straightforward applications. Ideal for high-volume, cost-sensitive deployments.',
pricing: {
prompt: pricing(`$0.05 / 1M tokens`),
output: pricing(`$0.40 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-4.1',
modelName: 'gpt-4.1',
modelDescription: 'Smartest non-reasoning model with 128K context window. Enhanced version of GPT-4 with improved instruction following, better factual accuracy, and reduced hallucinations. Features advanced function calling capabilities and superior performance on coding tasks. Ideal for applications requiring high intelligence without reasoning overhead.',
pricing: {
prompt: pricing(`$3.00 / 1M tokens`),
output: pricing(`$12.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-4.1-mini',
modelName: 'gpt-4.1-mini',
modelDescription: 'Smaller, faster version of GPT-4.1 with 128K context window. Balances intelligence and efficiency with 3x faster inference than base GPT-4.1. Maintains strong capabilities across text generation, reasoning, and coding while offering better cost-performance ratio for most applications.',
pricing: {
prompt: pricing(`$0.80 / 1M tokens`),
output: pricing(`$3.20 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-4.1-nano',
modelName: 'gpt-4.1-nano',
modelDescription: 'Fastest, most cost-efficient version of GPT-4.1 with 128K context window. Optimized for high-throughput applications requiring good quality at minimal cost. Features 5x faster inference than GPT-4.1 while maintaining adequate performance for most general-purpose tasks.',
pricing: {
prompt: pricing(`$0.20 / 1M tokens`),
output: pricing(`$0.80 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'o3',
modelName: 'o3',
modelDescription: 'Advanced reasoning model with 128K context window specializing in complex logical, mathematical, and analytical tasks. Successor to o1 with enhanced step-by-step problem-solving capabilities and superior performance on STEM-focused problems. Ideal for professional applications requiring deep analytical thinking and precise reasoning.',
pricing: {
prompt: pricing(`$15.00 / 1M tokens`),
output: pricing(`$60.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'o3-pro',
modelName: 'o3-pro',
modelDescription: 'Enhanced version of o3 with more compute allocated for better responses on the most challenging problems. Features extended reasoning time and improved accuracy on complex analytical tasks. Designed for applications where maximum reasoning quality is more important than response speed.',
pricing: {
prompt: pricing(`$30.00 / 1M tokens`),
output: pricing(`$120.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'o4-mini',
modelName: 'o4-mini',
modelDescription: 'Fast, cost-efficient reasoning model with 128K context window. Successor to o1-mini with improved analytical capabilities while maintaining speed advantages. Features enhanced mathematical reasoning and logical problem-solving at significantly lower cost than full reasoning models.',
pricing: {
prompt: pricing(`$4.00 / 1M tokens`),
output: pricing(`$16.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'o3-deep-research',
modelName: 'o3-deep-research',
modelDescription: 'Most powerful deep research model with 128K context window. Specialized for comprehensive research tasks, literature analysis, and complex information synthesis. Features advanced citation capabilities and enhanced factual accuracy for academic and professional research applications.',
pricing: {
prompt: pricing(`$25.00 / 1M tokens`),
output: pricing(`$100.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'o4-mini-deep-research',
modelName: 'o4-mini-deep-research',
modelDescription: 'Faster, more affordable deep research model with 128K context window. Balances research capabilities with cost efficiency, offering good performance on literature review, fact-checking, and information synthesis tasks at a more accessible price point.',
pricing: {
prompt: pricing(`$12.00 / 1M tokens`),
output: pricing(`$48.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'IMAGE_GENERATION',
modelTitle: 'dall-e-3',
modelName: 'dall-e-3',
modelDescription: 'DALLยทE 3 is the latest version of the DALLยทE art generation model. It understands significantly more nuance and detail than our previous systems, allowing you to easily translate your ideas into exceptionally accurate images.',
pricing: {
prompt: 0,
output: 0.04,
},
},
/**/
/*/
{
modelTitle: 'whisper-1',
modelName: 'whisper-1',
},
/**/
/**/
{
modelVariant: 'COMPLETION',
modelTitle: 'davinci-002',
modelName: 'davinci-002',
modelDescription: 'Legacy completion model with 4K token context window. Excels at complex text generation, creative writing, and detailed content creation with strong contextual understanding. Optimized for instructions requiring nuanced outputs and extended reasoning. Suitable for applications needing high-quality text generation without conversation management.',
pricing: {
prompt: pricing(`$2.00 / 1M tokens`),
output: pricing(`$2.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'IMAGE_GENERATION',
modelTitle: 'dall-e-2',
modelName: 'dall-e-2',
modelDescription: 'DALLยทE 2 is an AI system that can create realistic images and art from a description in natural language.',
pricing: {
prompt: 0,
output: 0.02,
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-3.5-turbo-16k',
modelName: 'gpt-3.5-turbo-16k',
modelDescription: 'Extended context GPT-3.5 Turbo with 16K token window. Maintains core capabilities of standard 3.5 Turbo while supporting longer conversations and documents. Features good balance of performance and cost for applications requiring more context than standard 4K models. Effective for document analysis, extended conversations, and multi-step reasoning tasks.',
pricing: {
prompt: pricing(`$3.00 / 1M tokens`),
output: pricing(`$4.00 / 1M tokens`),
},
},
/**/
/*/
{
modelTitle: 'tts-1-hd-1106',
modelName: 'tts-1-hd-1106',
},
/**/
/*/
{
modelTitle: 'tts-1-hd',
modelName: 'tts-1-hd',
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-4',
modelName: 'gpt-4',
modelDescription: 'Powerful language model with 8K context window featuring sophisticated reasoning, instruction-following, and knowledge capabilities. Demonstrates strong performance on complex tasks requiring deep understanding and multi-step reasoning. Excels at code generation, logical analysis, and nuanced content creation. Suitable for advanced applications requiring high-quality outputs.',
pricing: {
prompt: pricing(`$30.00 / 1M tokens`),
output: pricing(`$60.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-4-32k',
modelName: 'gpt-4-32k',
modelDescription: 'Extended context version of GPT-4 with 32K token window. Maintains all capabilities of standard GPT-4 while supporting analysis of very lengthy documents, code bases, and conversations. Features enhanced ability to maintain context over long interactions and process detailed information from large inputs. Ideal for document analysis, legal review, and complex problem-solving.',
pricing: {
prompt: pricing(`$60.00 / 1M tokens`),
output: pricing(`$120.00 / 1M tokens`),
},
},
/**/
/*/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-4-0613',
modelName: 'gpt-4-0613',
pricing: {
prompt: computeUsage(` / 1M tokens`),
output: computeUsage(` / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-4-turbo-2024-04-09',
modelName: 'gpt-4-turbo-2024-04-09',
modelDescription: 'Latest stable GPT-4 Turbo from April 2024 with 128K context window. Features enhanced reasoning chains, improved factual accuracy with 40% reduction in hallucinations, and better instruction following compared to earlier versions. Includes advanced function calling capabilities and knowledge up to April 2024. Provides optimal performance for enterprise applications requiring reliability.',
pricing: {
prompt: pricing(`$10.00 / 1M tokens`),
output: pricing(`$30.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-3.5-turbo-1106',
modelName: 'gpt-3.5-turbo-1106',
modelDescription: 'November 2023 version of GPT-3.5 Turbo with 16K token context window. Features improved instruction following, more consistent output formatting, and enhanced function calling capabilities. Includes knowledge cutoff from April 2023. Suitable for applications requiring good performance at lower cost than GPT-4 models.',
pricing: {
prompt: pricing(`$1.00 / 1M tokens`),
output: pricing(`$2.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-4-turbo',
modelName: 'gpt-4-turbo',
modelDescription: 'More capable and cost-efficient version of GPT-4 with 128K token context window. Features improved instruction following, advanced function calling capabilities, and better performance on coding tasks. Maintains superior reasoning and knowledge while offering substantial cost reduction compared to base GPT-4. Ideal for complex applications requiring extensive context processing.',
pricing: {
prompt: pricing(`$10.00 / 1M tokens`),
output: pricing(`$30.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'COMPLETION',
modelTitle: 'gpt-3.5-turbo-instruct-0914',
modelName: 'gpt-3.5-turbo-instruct-0914',
modelDescription: 'September 2023 version of GPT-3.5 Turbo Instruct with 4K context window. Optimized for completion-style instruction following with deterministic responses. Better suited than chat models for applications requiring specific formatted outputs without conversation management. Knowledge cutoff from September 2021.',
pricing: {
prompt: pricing(`$1.50 / 1M tokens`),
output: pricing(`$2.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'COMPLETION',
modelTitle: 'gpt-3.5-turbo-instruct',
modelName: 'gpt-3.5-turbo-instruct',
modelDescription: 'Optimized version of GPT-3.5 for completion-style API with 4K token context window. Features strong instruction following with single-turn design rather than multi-turn conversation. Provides more consistent, deterministic outputs compared to chat models. Well-suited for templated content generation and structured text transformation tasks.',
pricing: {
prompt: pricing(`$1.50 / 1M tokens`),
output: pricing(`$2.00 / 1M tokens`),
},
},
/**/
/*/
{
modelTitle: 'tts-1',
modelName: 'tts-1',
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-3.5-turbo',
modelName: 'gpt-3.5-turbo',
modelDescription: 'Latest version of GPT-3.5 Turbo with 4K token default context window (16K available). Features continually improved performance with enhanced instruction following and reduced hallucinations. Offers excellent balance between capability and cost efficiency. Suitable for most general-purpose applications requiring good AI capabilities at reasonable cost.',
pricing: {
prompt: pricing(`$0.50 / 1M tokens`),
output: pricing(`$1.50 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-3.5-turbo-0301',
modelName: 'gpt-3.5-turbo-0301',
modelDescription: 'March 2023 version of GPT-3.5 Turbo with 4K token context window. Legacy model maintained for backward compatibility with specific application behaviors. Features solid conversational abilities and basic instruction following. Knowledge cutoff from September 2021. Suitable for applications explicitly designed for this version.',
pricing: {
prompt: pricing(`$1.50 / 1M tokens`),
output: pricing(`$2.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'COMPLETION',
modelTitle: 'babbage-002',
modelName: 'babbage-002',
modelDescription: 'Efficient legacy completion model with 4K context window balancing performance and speed. Features moderate reasoning capabilities with focus on straightforward text generation tasks. Significantly more efficient than davinci models while maintaining adequate quality for many applications. Suitable for high-volume, cost-sensitive text generation needs.',
pricing: {
prompt: pricing(`$0.40 / 1M tokens`),
output: pricing(`$0.40 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-4-1106-preview',
modelName: 'gpt-4-1106-preview',
modelDescription: 'November 2023 preview version of GPT-4 Turbo with 128K token context window. Features improved instruction following, better function calling capabilities, and enhanced reasoning. Includes knowledge cutoff from April 2023. Suitable for complex applications requiring extensive document understanding and sophisticated interactions.',
pricing: {
prompt: pricing(`$10.00 / 1M tokens`),
output: pricing(`$30.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-4-0125-preview',
modelName: 'gpt-4-0125-preview',
modelDescription: 'January 2024 preview version of GPT-4 Turbo with 128K token context window. Features improved reasoning capabilities, enhanced tool use, and more reliable function calling. Includes knowledge cutoff from October 2023. Offers better performance on complex logical tasks and more consistent outputs than previous preview versions.',
pricing: {
prompt: pricing(`$10.00 / 1M tokens`),
output: pricing(`$30.00 / 1M tokens`),
},
},
/**/
/*/
{
modelTitle: 'tts-1-1106',
modelName: 'tts-1-1106',
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-3.5-turbo-0125',
modelName: 'gpt-3.5-turbo-0125',
modelDescription: 'January 2024 version of GPT-3.5 Turbo with 16K token context window. Features improved reasoning capabilities, better instruction adherence, and reduced hallucinations compared to previous versions. Includes knowledge cutoff from September 2021. Provides good performance for most general applications at reasonable cost.',
pricing: {
prompt: pricing(`$0.50 / 1M tokens`),
output: pricing(`$1.50 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-4-turbo-preview',
modelName: 'gpt-4-turbo-preview',
modelDescription: 'Preview version of GPT-4 Turbo with 128K token context window that points to the latest development model. Features cutting-edge improvements to instruction following, knowledge representation, and tool use capabilities. Provides access to newest features but may have occasional behavior changes. Best for non-critical applications wanting latest capabilities.',
pricing: {
prompt: pricing(`$10.00 / 1M tokens`),
output: pricing(`$30.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'EMBEDDING',
modelTitle: 'text-embedding-3-large',
modelName: 'text-embedding-3-large',
modelDescription: "OpenAI's most capable text embedding model generating 3072-dimensional vectors. Designed for high-quality embeddings for complex similarity tasks, clustering, and information retrieval. Features enhanced cross-lingual capabilities and significantly improved performance on retrieval and classification benchmarks. Ideal for sophisticated RAG systems and semantic search applications.",
pricing: {
prompt: pricing(`$0.13 / 1M tokens`),
output: 0,
},
},
/**/
/**/
{
modelVariant: 'EMBEDDING',
modelTitle: 'text-embedding-3-small',
modelName: 'text-embedding-3-small',
modelDescription: 'Cost-effective embedding model generating 1536-dimensional vectors. Balances quality and efficiency for simpler tasks while maintaining good performance on text similarity and retrieval applications. Offers 20% better quality than ada-002 at significantly lower cost. Ideal for production embedding applications with cost constraints.',
pricing: {
prompt: pricing(`$0.02 / 1M tokens`),
output: 0,
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-3.5-turbo-0613',
modelName: 'gpt-3.5-turbo-0613',
modelDescription: "June 2023 version of GPT-3.5 Turbo with 4K token context window. Features function calling capabilities for structured data extraction and API interaction. Includes knowledge cutoff from September 2021. Maintained for applications specifically designed for this version's behaviors and capabilities.",
pricing: {
prompt: pricing(`$1.50 / 1M tokens`),
output: pricing(`$2.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'EMBEDDING',
modelTitle: 'text-embedding-ada-002',
modelName: 'text-embedding-ada-002',
modelDescription: 'Legacy text embedding model generating 1536-dimensional vectors suitable for text similarity and retrieval applications. Processes up to 8K tokens per request with consistent embedding quality. While superseded by newer embedding-3 models, still maintains adequate performance for many semantic search and classification tasks.',
pricing: {
prompt: pricing(`$0.1 / 1M tokens`),
output: 0,
},
},
/**/
/*/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-4-1106-vision-preview',
modelName: 'gpt-4-1106-vision-preview',
},
/**/
/*/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-4-vision-preview',
modelName: 'gpt-4-vision-preview',
pricing: {
prompt: computeUsage(`$10.00 / 1M tokens`),
output: computeUsage(`$30.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-4o-2024-05-13',
modelName: 'gpt-4o-2024-05-13',
modelDescription: 'May 2024 version of GPT-4o with 128K context window. Features enhanced multimodal capabilities including superior image understanding (up to 20MP), audio processing, and improved reasoning. Optimized for 2x lower latency than GPT-4 Turbo while maintaining high performance. Includes knowledge up to October 2023. Ideal for production applications requiring reliable multimodal capabilities.',
pricing: {
prompt: pricing(`$5.00 / 1M tokens`),
output: pricing(`$15.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-4o',
modelName: 'gpt-4o',
modelDescription: "OpenAI's most advanced general-purpose multimodal model with 128K context window. Optimized for balanced performance, speed, and cost with 2x faster responses than GPT-4 Turbo. Features excellent vision processing, audio understanding, reasoning, and text generation quality. Represents optimal balance of capability and efficiency for most advanced applications.",
pricing: {
prompt: pricing(`$5.00 / 1M tokens`),
output: pricing(`$15.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-4o-mini',
modelName: 'gpt-4o-mini',
modelDescription: 'Smaller, more cost-effective version of GPT-4o with 128K context window. Maintains impressive capabilities across text, vision, and audio tasks while operating at significantly lower cost. Features 3x faster inference than GPT-4o with good performance on general tasks. Excellent for applications requiring good quality multimodal capabilities at scale.',
pricing: {
prompt: pricing(`$0.15 / 1M tokens`),
output: pricing(`$0.60 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'o1-preview',
modelName: 'o1-preview',
modelDescription: 'Advanced reasoning model with 128K context window specializing in complex logical, mathematical, and analytical tasks. Features exceptional step-by-step problem-solving capabilities, advanced mathematical and scientific reasoning, and superior performance on STEM-focused problems. Significantly outperforms GPT-4 on quantitative reasoning benchmarks. Ideal for professional and specialized applications.',
pricing: {
prompt: pricing(`$15.00 / 1M tokens`),
output: pricing(`$60.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'o1-preview-2024-09-12',
modelName: 'o1-preview-2024-09-12',
modelDescription: 'September 2024 version of O1 preview with 128K context window. Features specialized reasoning capabilities with 30% improvement on mathematical and scientific accuracy over previous versions. Includes enhanced support for formal logic, statistical analysis, and technical domains. Optimized for professional applications requiring precise analytical thinking and rigorous methodologies.',
pricing: {
prompt: pricing(`$15.00 / 1M tokens`),
output: pricing(`$60.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'o1-mini',
modelName: 'o1-mini',
modelDescription: 'Smaller, cost-effective version of the O1 model with 128K context window. Maintains strong analytical reasoning abilities while reducing computational requirements by 70%. Features good performance on mathematical, logical, and scientific tasks at significantly lower cost than full O1. Excellent for everyday analytical applications that benefit from reasoning focus.',
pricing: {
prompt: pricing(`$3.00 / 1M tokens`),
output: pricing(`$12.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'o1',
modelName: 'o1',
modelDescription: "OpenAI's advanced reasoning model with 128K context window focusing on logical problem-solving and analytical thinking. Features exceptional performance on quantitative tasks, step-by-step deduction, and complex technical problems. Maintains 95%+ of o1-preview capabilities with production-ready stability. Ideal for scientific computing, financial analysis, and professional applications.",
pricing: {
prompt: pricing(`$15.00 / 1M tokens`),
output: pricing(`$60.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'o3-mini',
modelName: 'o3-mini',
modelDescription: 'Cost-effective reasoning model with 128K context window optimized for academic and scientific problem-solving. Features efficient performance on STEM tasks with specialized capabilities in mathematics, physics, chemistry, and computer science. Offers 80% of O1 performance on technical domains at significantly lower cost. Ideal for educational applications and research support.',
pricing: {
prompt: pricing(`$3.00 / 1M tokens`),
output: pricing(`$12.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'o1-mini-2024-09-12',
modelName: 'o1-mini-2024-09-12',
modelDescription: "September 2024 version of O1-mini with 128K context window featuring balanced reasoning capabilities and cost-efficiency. Includes 25% improvement in mathematical accuracy and enhanced performance on coding tasks compared to previous versions. Maintains efficient resource utilization while delivering improved results for analytical applications that don't require the full O1 model.",
pricing: {
prompt: pricing(`$3.00 / 1M tokens`),
output: pricing(`$12.00 / 1M tokens`),
},
},
/**/
/**/
{
modelVariant: 'CHAT',
modelTitle: 'gpt-3.5-turbo-16k-0613',
modelName: 'gpt-3.5-turbo-16k-0613',
modelDescription: "June 2023 version of GPT-3.5 Turbo with extended 16K token context window. Features good handling of longer conversations and documents with improved memory management across extended contexts. Includes knowledge cutoff from September 2021. Maintained for applications specifically designed for this version's behaviors and capabilities.",
pricing: {
prompt: pricing(`$3.00 / 1M tokens`),
output: pricing(`$4.00 / 1M tokens`),
},
},
/**/
// <- [๐]
],
});
/**
* Note: [๐ค] Add models of new variant
* TODO: [๐ง ] Some mechanism to propagate unsureness
* TODO: [๐ฐ] Some mechanism to auto-update available models
* TODO: [๐ฐ][๐ฎโโ๏ธ] Make this list dynamic - dynamically can be listed modelNames but not modelVariant, legacy status, context length and pricing
* TODO: [๐ง ][๐ฎโโ๏ธ] Put here more info like description, isVision, trainingDateCutoff, languages, strengths ( Top-level performance, intelligence, fluency, and understanding), contextWindow,...
* @see https://platform.openai.com/docs/models/gpt-4-turbo-and-gpt-4
* @see https://openai.com/api/pricing/
* @see /other/playground/playground.ts
* TODO: [๐][๐ฉ] Make better
* TODO: Change model titles to human eg: "gpt-4-turbo-2024-04-09" -> "GPT-4 Turbo (2024-04-09)"
* TODO: [๐ธ] Not all models are compatible with JSON mode, add this information here and use it
* Note: [๐] Ignore a discrepancy between file name and entity name
*/
/**
* Computes the usage of the OpenAI API based on the response from OpenAI
*
* @param promptContent The content of the prompt
* @param resultContent The content of the result (for embedding prompts or failed prompts pass empty string)
* @param rawResponse The raw response from OpenAI API
* @throws {PipelineExecutionError} If the usage is not defined in the response from OpenAI
* @private internal utility of `OpenAiExecutionTools`
*/
function computeOpenAiUsage(promptContent, // <- Note: Intentionally using [] to access type properties to bring jsdoc from Prompt/PromptResult to consumer
resultContent, rawResponse) {
var _a, _b;
if (rawResponse.usage === undefined) {
throw new PipelineExecutionError('The usage is not defined in the response from OpenAI');
}
if (((_a = rawResponse.usage) === null || _a === void 0 ? void 0 : _a.prompt_tokens) === undefined) {
throw new PipelineExecutionError('In OpenAI response `usage.prompt_tokens` not defined');
}
const inputTokens = rawResponse.usage.prompt_tokens;
const outputTokens = ((_b = rawResponse.usage) === null || _b === void 0 ? void 0 : _b.completion_tokens) || 0;
let isUncertain = false;
let modelInfo = OPENAI_MODELS.find((model) => model.modelName === rawResponse.model);
if (modelInfo === undefined) {
// Note: Model is not in the list of known models, fallback to the family of the models and mark price as uncertain
modelInfo = OPENAI_MODELS.find((model) => (rawResponse.model || SALT_NONCE).startsWith(model.modelName));
if (modelInfo !== undefined) {
isUncertain = true;
}
}
let price;
if (modelInfo === undefined || modelInfo.pricing === undefined) {
price = uncertainNumber();
}
else {
price = uncertainNumber(inputTokens * modelInfo.pricing.prompt + outputTokens * modelInfo.pricing.output, isUncertain);
}
return {
price,
input: {
tokensCount: uncertainNumber(rawResponse.usage.prompt_tokens),
...computeUsageCounts(promptContent),
},
output: {
tokensCount: uncertainNumber(outputTokens),
...computeUsageCounts(resultContent),
},
};
}
/**
* TODO: [๐ค] DRY Maybe some common abstraction between `computeOpenAiUsage` and `computeAnthropicClaudeUsage`
*/
/**
* Function `addUsage` will add multiple usages into one
*
* Note: If you provide 0 values, it returns ZERO_USAGE
*
* @public exported from `@promptbook/core`
*/
function addUsage(...usageItems) {
return usageItems.reduce((acc, item) => {
var _a;
acc.price.value += ((_a = item.price) === null || _a === void 0 ? void 0 : _a.value) || 0;
for (const key of Object.keys(acc.input)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
if (item.input[key]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
acc.input[key].value += item.input[key].value || 0;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
if (item.input[key].isUncertain) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
acc.input[key].isUncertain = true;
}
}
}
for (const key of Object.keys(acc.output)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
if (item.output[key]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
acc.output[key].value += item.output[key].value || 0;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
if (item.output[key].isUncertain) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
acc.output[key].isUncertain = true;
}
}
}
return acc;
}, deepClone(ZERO_USAGE));
}
/**
* Maps Promptbook tools to OpenAI tools.
*
* @private
*/
function mapToolsToOpenAi(tools) {
return tools.map((tool) => ({
type: 'function',
function: {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
},
}));
}
/**
* Parses an OpenAI error message to identify which parameter is unsupported
*
* @param errorMessage The error message from OpenAI API
* @returns The parameter name that is unsupported, or null if not an unsupported parameter error
* @private utility of LLM Tools
*/
function parseUnsupportedParameterError(errorMessage) {
// Pattern to match "Unsupported value: 'parameter' does not support ..."
const unsupportedValueMatch = errorMessage.match(/Unsupported value:\s*'([^']+)'\s*does not support/i);
if (unsupportedValueMatch === null || unsupportedValueMatch === void 0 ? void 0 : unsupportedValueMatch[1]) {
return unsupportedValueMatch[1];
}
// Pattern to match "'parameter' of type ... is not supported with this model"
const parameterTypeMatch = errorMessage.match(/'([^']+)'\s*of type.*is not supported with this model/i);
if (parameterTypeMatch === null || parameterTypeMatch === void 0 ? void 0 : parameterTypeMatch[1]) {
return parameterTypeMatch[1];
}
return null;
}
/**
* Creates a copy of model requirements with the specified parameter removed
*
* @param modelRequirements Original model requirements
* @param unsupportedParameter The parameter to remove
* @returns New model requirements without the unsupported parameter
* @private utility of LLM Tools
*/
function removeUnsupportedModelRequirement(modelRequirements, unsupportedParameter) {
const newRequirements = { ...modelRequirements };
// Map of parameter names that might appear in error messages to ModelRequirements properties
const parameterMap = {
temperature: 'temperature',
max_tokens: 'maxTokens',
maxTokens: 'maxTokens',
seed: 'seed',
};
const propertyToRemove = parameterMap[unsupportedParameter];
if (propertyToRemove && propertyToRemove in newRequirements) {
delete newRequirements[propertyToRemove];
}
return newRequirements;
}
/**
* Checks if an error is an "Unsupported value" error from OpenAI
* @param error The error to check
* @returns true if this is an unsupported parameter error
* @private utility of LLM Tools
*/
function isUnsupportedParameterError(error) {
const errorMessage = error.message.toLowerCase();
return (errorMessage.includes('unsupported value:') ||
errorMessage.includes('is not supported with this model') ||
errorMessage.includes('does not support'));
}
/**
* Execution Tools for calling OpenAI API or other OpenAI compatible provider
*
* @public exported from `@promptbook/openai`
*/
class OpenAiCompatibleExecutionTools {
// Removed retriedUnsupportedParameters and attemptHistory instance fields
/**
* Creates OpenAI compatible Execution Tools.
*
* @param options which are relevant are directly passed to the OpenAI compatible client
*/
constructor(options) {
this.options = options;
/**
* OpenAI API client.
*/
this.client = null;
// TODO: Allow configuring rate limits via options
this.limiter = new Bottleneck({
minTime: 60000 / (this.options.maxRequestsPerMinute || DEFAULT_MAX_REQUESTS_PER_MINUTE),
});
}
async getClient() {
if (this.client === null) {
// Note: Passing only OpenAI relevant options to OpenAI constructor
const openAiOptions = { ...this.options };
delete openAiOptions.isVerbose;
delete openAiOptions.userId;
// Enhanced configuration for better ECONNRESET handling
const enhancedOptions = {
...openAiOptions,
timeout: API_REQUEST_TIMEOUT,
maxRetries: CONNECTION_RETRIES_LIMIT,
defaultHeaders: {
Connection: 'keep-alive',
'Keep-Alive': 'timeout=30, max=100',
...openAiOptions.defaultHeaders,
},
};
this.client = new OpenAI(enhancedOptions);
}
return this.client;
}
/**
* Check the `options` passed to `constructor`
*/
async checkConfiguration() {
await this.getClient();
// TODO: [๐] Do here a real check that API is online, working and API key is correct
}
/**
* List all available OpenAI compatible models that can be used
*/
async listModels() {
const client = await this.getClient();
const rawModelsList = await client.models.list();
const availableModels = rawModelsList.data
.sort((a, b) => (a.created > b.created ? 1 : -1))
.map((modelFromApi) => {
const modelFromList = this.HARDCODED_MODELS.find(({ modelName }) => modelName === modelFromApi.id ||
modelName.startsWith(modelFromApi.id) ||
modelFromApi.id.startsWith(modelName));
if (modelFromList !== undefined) {
return modelFromList;
}
return {
modelVariant: 'CHAT',
modelTitle: modelFromApi.id,
modelName: modelFromApi.id,
modelDescription: '',
};
});
return availableModels;
}
/**
* Calls OpenAI compatible API to use a chat model.
*/
/**
* Calls OpenAI compatible API to use a chat model.
*/
async callChatModel(prompt) {
return this.callChatModelStream(prompt, () => { });
}
/**
* Calls OpenAI compatible API to use a chat model with streaming.
*/
async callChatModelStream(prompt, onProgress) {
// Deep clone prompt and modelRequirements to avoid mutation across calls
const clonedPrompt = JSON.parse(JSON.stringify(prompt));
// Use local Set for retried parameters to ensure independence and thread safety
const retriedUnsupportedParameters = new Set();
return this.callChatModelWithRetry(clonedPrompt, clonedPrompt.modelRequirements, [], retriedUnsupportedParameters, onProgress);
}
/**
* Internal method that handles parameter retry for chat model calls
*/
async callChatModelWithRetry(prompt, currentModelRequirements, attemptStack = [], retriedUnsupportedParameters = new Set(), onProgress) {
var _a;
if (this.options.isVerbose) {
console.info(`๐ฌ ${this.title} callChatModel call`, { prompt, currentModelRequirements });
}
const { content, parameters, format } = prompt;
const client = await this.getClient();
// TODO: [โ] Use here more modelRequirements
if (currentModelRequirements.modelVariant !== 'CHAT') {
throw new PipelineExecutionError('Use callChatModel only for CHAT variant');
}
const modelName = currentModelRequirements.modelName || this.getDefaultChatModel().modelName;
const modelSettings = {
model: modelName,
max_tokens: currentModelRequirements.maxTokens,
temperature: currentModelRequirements.temperature,
// <- TODO: [๐] Use `seed` here AND/OR use is `isDeterministic` for entire execution tools
// <- Note: [๐ง]
}; // <- TODO: [๐ฉ] Guard here types better
if (format === 'JSON') {
modelSettings.response_format = {
type: 'json_object',
};
}
// <- TODO: [๐ธ] Not all models are compatible with JSON mode
// > 'response_format' of type 'json_object' is not supported with this model.
const rawPromptContent = templateParameters(content, { ...parameters, modelName });
// Convert thread to OpenAI format if present
let threadMessages = [];
if ('thread' in prompt && Array.isArray(prompt.thread)) {
threadMessages = prompt.thread.map((msg) => ({
role: msg.sender === 'assistant' ? 'assistant' : 'user',
content: msg.content,
}));
}
const messages = [
...(currentModelRequirements.systemMessage === undefined
? []
: [
{
role: 'system',
content: currentModelRequirements.systemMessage,
},
]),
...threadMessages,
];
if ('files' in prompt && Array.isArray(prompt.files) && prompt.files.length > 0) {
const filesContent = await Promise.all(prompt.files.map(async (file) => {
const arrayBuffer = await file.arrayBuffer();
const base64 = Buffer.from(arrayBuffer).toString('base64');
return {
type: 'image_url',
image_url: {
url: `data:${file.type};base64,${base64}`,
},
};
}));
messages.push({
role: 'user',
content: [
{
type: 'text',
text: rawPromptContent,
},
...filesContent,
],
});
}
else {
messages.push({
role: 'user',
content: rawPromptContent,
});
}
let totalUsage = {
price: uncertainNumber(0),
input: {
tokensCount: uncertainNumber(0),
charactersCount: uncertainNumber(0),
wordsCount: uncertainNumber(0),
sentencesCount: uncertainNumber(0),
linesCount: uncertainNumber(0),
paragraphsCount: uncertainNumber(0),
pagesCount: uncertainNumber(0),
},
output: {
tokensCount: uncertainNumber(0),
charactersCount: uncertainNumber(0),
wordsCount: uncertainNumber(0),
sentencesCount: uncertainNumber(0),
linesCount: uncertainNumber(0),
paragraphsCount: uncertainNumber(0),
pagesCount: uncertainNumber(0),
},
};
const toolCalls = [];
const start = $getCurrentDate();
const tools = 'tools' in prompt && Array.isArray(prompt.tools) ? prompt.tools : currentModelRequirements.tools;
let isLooping = true;
while (isLooping) {
const rawRequest = {
...modelSettings,
messages,
user: (_a = this.options.userId) === null || _a === void 0 ? void 0 : _a.toString(),
tools: tools === undefined ? undefined : mapToolsToOpenAi(tools),
};
if (this.options.isVerbose) {
console.info(colors.bgWhite('rawRequest'), JSON.stringify(rawRequest, null, 4));
}
try {
const rawResponse = await this.limiter
.schedule(() => this.makeRequestWithNetworkRetry(() => client.chat.completions.create(rawRequest)))
.catch((error) => {
assertsError(error);
if (this.options.isVerbose) {
console.info(colors.bgRed('error'), error);
}
throw error;
});
if (this.options.isVerbose) {
console.info(colors.bgWhite('rawResponse'), JSON.stringify(rawResponse, null, 4));
}
if (!rawResponse.choices[0]) {
throw new PipelineExecutionError(`No choises from ${this.title}`);
}
const responseMessage = rawResponse.choices[0].message;
messages.push(responseMessage);
const usage = this.computeUsage(content || '', responseMessage.content || '', rawResponse);
totalUsage = addUsage(totalUsage, usage);
if (responseMessage.tool_calls && responseMessage.tool_calls.length > 0) {
const toolCallStartedAt = new Map();
if (onProgress) {
onProgress({
content: responseMessage.content || '',
modelName: rawResponse.model || modelName,
timing: { start, complete: $getCurrentDate() },
usage: totalUsage,
toolCalls: responseMessage.tool_calls.map((toolCall) => {
const calledAt = $getCurrentDate();
if (toolCall.id) {
toolCallStartedAt.set(toolCall.id, calledAt);
}
return {
name: toolCall.function.name,
arguments: toolCall.function.arguments,
result: '',
rawToolCall: toolCall,
createdAt: calledAt,
};
}),
rawPromptContent,
rawRequest,
rawResponse,
});
}
await forEachAsync(responseMessage.tool_calls, {}, async (toolCall) => {
const functionName = toolCall.function.name;
const functionArgs = toolCall.function.arguments;
const calledAt = toolCall.id
? toolCallStartedAt.get(toolCall.id) || $getCurrentDate()
: $getCurrentDate();
const executionTools = this.options
.executionTools;
if (!executionTools || !executionTools.script) {
throw new PipelineExecutionError(`Model requested tool '${functionName}' but no executionTools.script were provided in OpenAiCompatibleExecutionTools options`);
}
// TODO: [DRY] Use some common tool caller
const scriptTools = Array.isArray(executionTools.script)
? executionTools.script
: [executionTools.script];
let functionResponse;
let errors;
try {
const scriptTool = scriptTools[0]; // <- TODO: [๐ง ] Which script tool to use?
functionResponse = await scriptTool.execute({
scriptLanguage: 'javascript',
script: `
const args = ${functionArgs};
return await ${functionName}(args);
`,
parameters: prompt.parameters,
});
}
catch (error) {
assertsError(error);
functionResponse = `Error: ${error.message}`;
errors = [serializeError(error)];
}
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: functionResponse,
});
toolCalls.push({
name: functionName,
arguments: functionArgs,
result: functionResponse,
rawToolCall: toolCall,
createdAt: calledAt,
errors,
});
});
continue;
}
const complete = $getCurrentDate();
const resultContent = responseMessage.content;
if (resultContent === null) {
throw new PipelineExecutionError(`No response message from ${this.title}`);
}
isLooping = false;
return exportJson({
name: 'promptResult',
message: `Result of \`OpenAiCompatibleExecutionTools.callChatModel\``,
order: [],
value: {
content: resultContent,
modelName: rawResponse.model || modelName,
timing: {
start,
complete,
},
usage: totalUsage,
toolCalls,
rawPromptContent,
rawRequest,
rawResponse,
},
});
}
catch (error) {
isLooping = false;
assertsError(error);
// Check if this is an unsupported parameter error
if (!isUnsupportedParameterError(error)) {
// If we have attemptStack, include it in the error message
if (attemptStack.length > 0) {
throw new PipelineExecutionError(`All attempts failed. Attempt history:\n` +
attemptStack
.map((a, i) => ` ${i + 1}. Model: ${a.modelName}` +
(a.unsupportedParameter ? `, Stripped: ${a.unsupportedParameter}` : '') +
`, Error: ${a.errorMessage}` +
(a.stripped ? ' (stripped and retried)' : ''))
.join('\n') +
`\nFinal error: ${error.message}`);
}
throw error;
}
// Parse which parameter is unsupported
const unsupportedParameter = parseUnsupportedParameterError(error.message);
if (!unsupportedParameter) {
if (this.options.isVerbose) {
console.warn(colors.bgYellow('Warning'), 'Could not parse unsupported parameter from error:', error.message);
}
throw error;
}
// Create a unique key for this model + parameter combination to prevent infinite loops
const retryKey = `${modelName}-${unsupportedParameter}`;
if (retriedUnsupportedParameters.has(retryKey)) {
// Already retried this parameter, throw the error with attemptStack
attemptStack.push({
modelName,
unsupportedParameter,
errorMessage: error.message,
stripped: true,
});
throw new PipelineExecutionError(`All attempts failed. Attempt history:\n` +
attemptStack
.map((a, i) => ` ${i + 1}. Model: ${a.modelName}` +
(a.unsupportedParameter ? `, Stripped: ${a.unsupportedParameter}` : '') +
`, Error: ${a.errorMessage}` +
(a.stripped ? ' (stripped and retried)' : ''))
.join('\n') +
`\nFinal error: ${error.message}`);
}
// Mark this parameter as retried
retriedUnsupportedParameters.add(retryKey);
// Log warning in verbose mode
if (this.options.isVerbose) {
console.warn(colors.bgYellow('Warning'), `Removing unsupported parameter '${unsupportedParameter}' for model '${modelName}' and retrying request`);
}
// Add to attemptStack
attemptStack.push({
modelName,
unsupportedParameter,
errorMessage: error.message,
stripped: true,
});
// Remove the unsupported parameter and retry
const modifiedModelRequirements = removeUnsupportedModelRequirement(currentModelRequirements, unsupportedParameter);
return this.callChatModelWithRetry(prompt, modifiedModelRequirements, attemptStack, retriedUnsupportedParameters, onProgress);
}
}
throw new PipelineExecutionError(`Tool calling loop did not return a result from ${this.title}`);
}
/**
* Calls OpenAI API to use a complete model.
*/
async callCompletionModel(prompt) {
// Deep clone prompt and modelRequirements to avoid mutation across calls
const clonedPrompt = JSON.parse(JSON.stringify(prompt));
const retriedUnsupportedParameters = new Set();
return this.callCompletionModelWithRetry(clonedPrompt, clonedPrompt.modelRequirements, [], retriedUnsupportedParameters);
}
/**
* Internal method that handles parameter retry for completion model calls
*/
async callCompletionModelWithRetry(prompt, currentModelRequirements, attemptStack = [], retriedUnsupportedParameters = new Set()) {
var _a;
if (this.options.isVerbose) {
console.info(`๐ ${this.title} callCompletionModel call`, { prompt, currentModelRequirements });
}
const { content, parameters } = prompt;
const client = await this.getClient();
// TODO: [โ] Use here more modelRequirements
if (currentModelRequirements.modelVariant !== 'COMPLETION') {
throw new PipelineExecutionError('Use callCompletionModel only for COMPLETION variant');
}
const modelName = currentModelRequirements.modelName || this.getDefaultCompletionModel().modelName;
const modelSettings = {
model: modelName,
max_tokens: currentModelRequirements.maxTokens,
temperature: currentModelRequirements.temperature,
};
const rawPromptContent = templateParameters(content, { ...parameters, modelName });
const rawRequest = {
...modelSettings,
model: modelName,
prompt: rawPromptContent,
user: (_a = this.options.userId) === null || _a === void 0 ? void 0 : _a.toString(),
};
const start = $getCurrentDate();
if (this.options.isVerbose) {
console.info(colors.bgWhite('rawRequest'), JSON.stringify(rawRequest, null, 4));
}
try {
const rawResponse = await this.limiter
.schedule(() => this.makeRequestWithNetworkRetry(() => client.completions.create(rawRequest)))
.catch((error) => {
assertsError(error);
if (this.options.isVerbose) {
console.info(colors.bgRed('error'), error);
}
throw error;
});
if (this.options.isVerbose) {
console.info(colors.bgWhite('rawResponse'), JSON.stringify(rawResponse, null, 4));
}
const complete = $getCurrentDate();
if (!rawResponse.choices[0]) {
throw new PipelineExecutionError(`No choises from ${this.title}`);
}
if (rawResponse.choices.length > 1) {
throw new PipelineExecutionError(`More than one choise from ${this.title}`);
}
const resultContent = rawResponse.choices[0].text;
const usage = this.computeUsage(content || '', resultContent || '', rawResponse);
return exportJson({
name: 'promptResult',
message: `Result of \`OpenAiCompatibleExecutionTools.callCompletionModel\``,
order: [],
value: {
content: resultContent,
modelName: rawResponse.model || modelName,
timing: {
start,
complete,
},
usage,
rawPromptContent,
rawRequest,
rawResponse,
},
});
}
catch (error) {
assertsError(error);
if (!isUnsupportedParameterError(error)) {
if (attemptStack.length > 0) {
throw new PipelineExecutionError(`All attempts failed. Attempt history:\n` +
attemptStack
.map((a, i) => ` ${i + 1}. Model: ${a.modelName}` +
(a.unsupportedParameter ? `, Stripped: ${a.unsupportedParameter}` : '') +
`, Error: ${a.errorMessage}` +
(a.stripped ? ' (stripped and retried)' : ''))
.join('\n') +
`\nFinal error: ${error.message}`);
}
throw error;
}
const unsupportedParameter = parseUnsupportedParameterError(error.message);
if (!unsupportedParameter) {
if (this.options.isVerbose) {
console.warn(colors.bgYellow('Warning'), 'Could not parse unsupported parameter from error:', error.message);
}
throw error;
}
const retryKey = `${modelName}-${unsupportedParameter}`;
if (retriedUnsupportedParameters.has(retryKey)) {
attemptStack.push({
modelName,
unsupportedParameter,
errorMessage: error.message,
stripped: true,
});
throw new PipelineExecutionError(`All attempts failed. Attempt history:\n` +
attemptStack
.map((a, i) => ` ${i + 1}. Model: ${a.modelName}` +
(a.unsupportedParameter ? `, Stripped: ${a.unsupportedParameter}` : '') +
`, Error: ${a.errorMessage}` +
(a.stripped ? ' (stripped and retried)' : ''))
.join('\n') +
`\nFinal error: ${error.message}`);
}
retriedUnsupportedParameters.add(retryKey);
if (this.options.isVerbose) {
console.warn(colors.bgYellow('Warning'), `Removing unsupported parameter '${unsupportedParameter}' for model '${modelName}' and retrying request`);
}
attemptStack.push({
modelName,
unsupportedParameter,
errorMessage: error.message,
stripped: true,
});
const modifiedModelRequirements = removeUnsupportedModelRequirement(currentModelRequirements, unsupportedParameter);
return this.callCompletionModelWithRetry(prompt, modifiedModelRequirements, attemptStack, retriedUnsupportedParameters);
}
}
/**
* Calls OpenAI compatible API to use a embedding model
*/
async callEmbeddingModel(prompt) {
// Deep clone prompt and modelRequirements to avoid mutation across calls
const clonedPrompt = JSON.parse(JSON.stringify(prompt));
const retriedUnsupportedParameters = new Set();
return this.callEmbeddingModelWithRetry(clonedPrompt, clonedPrompt.modelRequirements, [], retriedUnsupportedParameters);
}
/**
* Internal method that handles parameter retry for embedding model calls
*/
async callEmbeddingModelWithRetry(prompt, currentModelRequirements, attemptStack = [], retriedUnsupportedParameters = new Set()) {
if (this.options.isVerbose) {
console.info(`๐ ${this.title} embedding call`, { prompt, currentModelRequirements });
}
const { content, parameters } = prompt;
const client = await this.getClient();
if (currentModelRequirements.modelVariant !== 'EMBEDDING') {
throw new PipelineExecutionError('Use embed only for EMBEDDING variant');
}
const modelName = currentModelRequirements.modelName || this.getDefaultEmbeddingModel().modelName;
const rawPromptContent = templateParameters(content, { ...parameters, modelName });
const rawRequest = {
input: rawPromptContent,
model: modelName,
};
const start = $getCurrentDate();
if (this.options.isVerbose) {
console.info(colors.bgWhite('rawRequest'), JSON.stringify(rawRequest, null, 4));
}
try {
const rawResponse = await this.limiter
.schedule(() => this.makeRequestWithNetworkRetry(() => client.embeddings.create(rawRequest)))
.catch((error) => {
assertsError(error);
if (this.options.isVerbose) {
console.info(colors.bgRed('error'), error);
}
throw error;
});
if (this.options.isVerbose) {
console.info(colors.bgWhite('rawResponse'), JSON.stringify(rawResponse, null, 4));
}
const complete = $getCurrentDate();
if (rawResponse.data.length !== 1) {
throw new PipelineExecutionError(`Expected exactly 1 data item in response, got ${rawResponse.data.length}`);
}
const resultContent = rawResponse.data[0].embedding;
const usage = this.computeUsage(content || '', '', rawResponse);
return exportJson({
name: 'promptResult',
message: `Result of \`OpenAiCompatibleExecutionTools.callEmbeddingModel\``,
order: [],
value: {
content: resultContent,
modelName: rawResponse.model || modelName,
timing: {
start,
complete,
},
usage,
rawPromptContent,
rawRequest,
rawResponse,
},
});
}
catch (error) {
assertsError(error);
if (!isUnsupportedParameterError(error)) {
if (attemptStack.length > 0) {
throw new PipelineExecutionError(`All attempts failed. Attempt history:\n` +
attemptStack
.map((a, i) => ` ${i + 1}. Model: ${a.modelName}` +
(a.unsupportedParameter ? `, Stripped: ${a.unsupportedParameter}` : '') +
`, Error: ${a.errorMessage}` +
(a.stripped ? ' (stripped and retried)' : ''))
.join('\n') +
`\nFinal error: ${error.message}`);
}
throw error;
}
const unsupportedParameter = parseUnsupportedParameterError(error.message);
if (!unsupportedParameter) {
if (this.options.isVerbose) {
console.warn(colors.bgYellow('Warning'), 'Could not parse unsupported parameter from error:', error.message);
}
throw error;
}
const retryKey = `${modelName}-${unsupportedParameter}`;
if (retriedUnsupportedParameters.has(retryKey)) {
attemptStack.push({
modelName,
unsupportedParameter,
errorMessage: error.message,
stripped: true,
});
throw new PipelineExecutionError(`All attempts failed. Attempt history:\n` +
attemptStack
.map((a, i) => ` ${i + 1}. Model: ${a.modelName}` +
(a.unsupportedParameter ? `, Stripped: ${a.unsupportedParameter}` : '') +
`, Error: ${a.errorMessage}` +
(a.stripped ? ' (stripped and retried)' : ''))
.join('\n') +
`\nFinal error: ${error.message}`);
}
retriedUnsupportedParameters.add(retryKey);
if (this.options.isVerbose) {
console.warn(colors.bgYellow('Warning'), `Removing unsupported parameter '${unsupportedParameter}' for model '${modelName}' and retrying request`);
}
attemptStack.push({
modelName,
unsupportedParameter,
errorMessage: error.message,
stripped: true,
});
const modifiedModelRequirements = removeUnsupportedModelRequirement(currentModelRequirements, unsupportedParameter);
return this.callEmbeddingModelWithRetry(prompt, modifiedModelRequirements, attemptStack, retriedUnsupportedParameters);
}
}
/**
* Calls OpenAI compatible API to use a image generation model
*/
async callImageGenerationModel(prompt) {
// Deep clone prompt and modelRequirements to avoid mutation across calls
const clonedPrompt = JSON.parse(JSON.stringify(prompt));
const retriedUnsupportedParameters = new Set();
return this.callImageGenerationModelWithRetry(clonedPrompt, clonedPrompt.modelRequirements, [], retriedUnsupportedParameters);
}
/**
* Internal method that handles parameter retry for image generation model calls
*/
async callImageGenerationModelWithRetry(prompt, currentModelRequirements, attemptStack = [], retriedUnsupportedParameters = new Set()) {
var _a, _b;
if (this.options.isVerbose) {
console.info(`๐จ ${this.title} callImageGenerationModel call`, { prompt, currentModelRequirements });
}
const { content, parameters } = prompt;
const client = await this.getClient();
// TODO: [โ] Use here more modelRequirements
if (currentModelRequirements.modelVariant !== 'IMAGE_GENERATION') {
throw new PipelineExecutionError('Use callImageGenerationModel only for IMAGE_GENERATION variant');
}
const modelName = currentModelRequirements.modelName || this.getDefaultImageGenerationModel().modelName;
const modelSettings = {
model: modelName,
size: currentModelRequirements.size,
quality: currentModelRequirements.quality,
style: currentModelRequirements.style,
};
const rawPromptContent = templateParameters(content, { ...parameters, modelName });
const rawRequest = {
...modelSettings,
prompt: rawPromptContent,
size: modelSettings.size || '1024x1024',
user: (_a = this.options.userId) === null || _a === void 0 ? void 0 : _a.toString(),
response_format: 'url', // TODO: [๐ง ] Maybe allow b64_json
};
const start = $getCurrentDate();
if (this.options.isVerbose) {
console.info(colors.bgWhite('rawRequest'), JSON.stringify(rawRequest, null, 4));
}
try {
const rawResponse = await this.limiter
.schedule(() => this.makeRequestWithNetworkRetry(() => client.images.generate(rawRequest)))
.catch((error) => {
assertsError(error);
if (this.options.isVerbose) {
console.info(colors.bgRed('error'), error);
}
throw error;
});
if (this.options.isVerbose) {
console.info(colors.bgWhite('rawResponse'), JSON.stringify(rawResponse, null, 4));
}
const complete = $getCurrentDate();
if (!rawResponse.data[0]) {
throw new PipelineExecutionError(`No choises from ${this.title}`);
}
if (rawResponse.data.length > 1) {
throw new PipelineExecutionError(`More than one choise from ${this.title}`);
}
const resultContent = rawResponse.data[0].url;
const modelInfo = this.HARDCODED_MODELS.find((model) => model.modelName === modelName);
const price = ((_b = modelInfo === null || modelInfo === void 0 ? void 0 : modelInfo.pricing) === null || _b === void 0 ? void 0 : _b.output) ? uncertainNumber(modelInfo.pricing.output) : uncertainNumber();
return exportJson({
name: 'promptResult',
message: `Result of \`OpenAiCompatibleExecutionTools.callImageGenerationModel\``,
order: [],
value: {
content: resultContent,
modelName: modelName,
timing: {
start,
complete,
},
usage: {
price,
input: {
tokensCount: uncertainNumber(0),
...computeUsageCounts(rawPromptContent),
},
output: {
tokensCount: uncertainNumber(0),
...computeUsageCounts(''),
},
},
rawPromptContent,
rawRequest,
rawResponse,
},
});
}
catch (error) {
assertsError(error);
if (!isUnsupportedParameterError(error)) {
if (attemptStack.length > 0) {
throw new PipelineExecutionError(`All attempts failed. Attempt history:\n` +
attemptStack
.map((a, i) => ` ${i + 1}. Model: ${a.modelName}` +
(a.unsupportedParameter ? `, Stripped: ${a.unsupportedParameter}` : '') +
`, Error: ${a.errorMessage}` +
(a.stripped ? ' (stripped and retried)' : ''))
.join('\n') +
`\nFinal error: ${error.message}`);
}
throw error;
}
const unsupportedParameter = parseUnsupportedParameterError(error.message);
if (!unsupportedParameter) {
if (this.options.isVerbose) {
console.warn(colors.bgYellow('Warning'), 'Could not parse unsupported parameter from error:', error.message);
}
throw error;
}
const retryKey = `${modelName}-${unsupportedParameter}`;
if (retriedUnsupportedParameters.has(retryKey)) {
attemptStack.push({
modelName,
unsupportedParameter,
errorMessage: error.message,
stripped: true,
});
throw new PipelineExecutionError(`All attempts failed. Attempt history:\n` +
attemptStack
.map((a, i) => ` ${i + 1}. Model: ${a.modelName}` +
(a.unsupportedParameter ? `, Stripped: ${a.unsupportedParameter}` : '') +
`, Error: ${a.errorMessage}` +
(a.stripped ? ' (stripped and retried)' : ''))
.join('\n') +
`\nFinal error: ${error.message}`);
}
retriedUnsupportedParameters.add(retryKey);
if (this.options.isVerbose) {
console.warn(colors.bgYellow('Warning'), `Removing unsupported parameter '${unsupportedParameter}' for model '${modelName}' and retrying request`);
}
attemptStack.push({
modelName,
unsupportedParameter,
errorMessage: error.message,
stripped: true,
});
const modifiedModelRequirements = removeUnsupportedModelRequirement(currentModelRequirements, unsupportedParameter);
return this.callImageGenerationModelWithRetry(prompt, modifiedModelRequirements, attemptStack, retriedUnsupportedParameters);
}
}
// <- Note: [๐ค] callXxxModel
/**
* Get the model that should be used as default
*/
getDefaultModel(defaultModelName) {
// Note: Match exact or prefix for model families
const model = this.HARDCODED_MODELS.find(({ modelName }) => modelName === defaultModelName || modelName.startsWith(defaultModelName));
if (model === undefined) {
throw new PipelineExecutionError(spaceTrim$2((block) => `
Cannot find model in ${this.title} models with name "${defaultModelName}" which should be used as default.
Available models:
${block(this.HARDCODED_MODELS.map(({ modelName }) => `- "${modelName}"`).join('\n'))}
Model "${defaultModelName}" is probably not available anymore, not installed, inaccessible or misconfigured.
`));
}
return model;
}
// <- Note: [๐ค] getDefaultXxxModel
/**
* Makes a request with retry logic for network errors like ECONNRESET
*/
async makeRequestWithNetworkRetry(requestFn) {
let lastError;
for (let attempt = 1; attempt <= CONNECTION_RETRIES_LIMIT; attempt++) {
try {
return await requestFn();
}
catch (error) {
assertsError(error);
lastError = error;
// Check if this is a retryable network error
const isRetryableError = this.isRetryableNetworkError(error);
if (!isRetryableError || attempt === CONNECTION_RETRIES_LIMIT) {
if (this.options.isVerbose && this.isRetryableNetworkError(error)) {
console.info(colors.bgRed('Final network error after retries'), `Attempt ${attempt}/${CONNECTION_RETRIES_LIMIT}:`, error);
}
throw error;
}
// Calculate exponential backoff delay
const baseDelay = 1000; // 1 second
const backoffDelay = baseDelay * Math.pow(2, attempt - 1);
const jitterDelay = Math.random() * 500; // Add some randomness
const totalDelay = backoffDelay + jitterDelay;
if (this.options.isVerbose) {
console.info(colors.bgYellow('Retrying network request'), `Attempt ${attempt}/${CONNECTION_RETRIES_LIMIT}, waiting ${Math.round(totalDelay)}ms:`, error.message);
}
// Wait before retrying
await new Promise((resolve) => setTimeout(resolve, totalDelay));
}
}
throw lastError;
}
/**
* Determines if an error is retryable (network-related errors)
*/
isRetryableNetworkError(error) {
const errorMessage = error.message.toLowerCase();
const errorCode = error.code;
// Network connection errors that should be retried
const retryableErrors = [
'econnreset',
'enotfound',
'econnrefused',
'etimedout',
'socket hang up',
'network error',
'fetch failed',
'connection reset',
'connection refused',
'timeout',
];
// Check error message
if (retryableErrors.some((retryableError) => errorMessage.includes(retryableError))) {
return true;
}
// Check error code
if (errorCode && retryableErrors.includes(errorCode.toLowerCase())) {
return true;
}
// Check for specific HTTP status codes that are retryable
const errorWithStatus = error;
const httpStatus = errorWithStatus.status || errorWithStatus.statusCode;
if (httpStatus && [429, 500, 502, 503, 504].includes(httpStatus)) {
return true;
}
return false;
}
}
/**
* TODO: [๐] Some way how to re-wrap the errors from `OpenAiCompatibleExecutionTools`
* TODO: [๐] Maybe make custom `OpenAiCompatibleError`
* TODO: [๐ง ][๐] Maybe use `isDeterministic` from options
* TODO: [๐ง ][๐ฐ] Allow to pass `title` for tracking purposes
* TODO: [๐ง ][๐ฆข] Make reverse adapter from LlmExecutionTools to OpenAI-compatible:
*/
/**
* Profile for OpenAI provider
*/
const OPENAI_PROVIDER_PROFILE = {
name: 'OPENAI',
fullname: 'OpenAI GPT',
color: '#10a37f',
};
/**
* Execution Tools for calling OpenAI API
*
* @public exported from `@promptbook/openai`
*/
class OpenAiExecutionTools extends OpenAiCompatibleExecutionTools {
constructor() {
super(...arguments);
/**
* Computes the usage of the OpenAI API based on the response from OpenAI
*/
this.computeUsage = computeOpenAiUsage;
// <- Note: [๐ค] getDefaultXxxModel
}
/* <- TODO: [๐] `, Destroyable` */
get title() {
return 'OpenAI';
}
get description() {
return 'Use all models provided by OpenAI';
}
get profile() {
return OPENAI_PROVIDER_PROFILE;
}
/*
Note: Commenting this out to avoid circular dependency
/**
* Create (sub)tools for calling OpenAI API Assistants
*
* @param assistantId Which assistant to use
* @returns Tools for calling OpenAI API Assistants with same token
* /
public createAssistantSubtools(assistantId: string_token): OpenAiAssistantExecutionTools {
return new OpenAiAssistantExecutionTools({ ...this.options, assistantId });
}
*/
/**
* List all available models (non dynamically)
*
* Note: Purpose of this is to provide more information about models than standard listing from API
*/
get HARDCODED_MODELS() {
return OPENAI_MODELS;
}
/**
* Default model for chat variant.
*/
getDefaultChatModel() {
return this.getDefaultModel('gpt-5');
}
/**
* Default model for completion variant.
*/
getDefaultCompletionModel() {
return this.getDefaultModel('gpt-3.5-turbo-instruct');
}
/**
* Default model for completion variant.
*/
getDefaultEmbeddingModel() {
return this.getDefaultModel('text-embedding-3-large');
}
/**
* Default model for image generation variant.
*/
getDefaultImageGenerationModel() {
return this.getDefaultModel('dall-e-3');
}
}
/**
* Uploads files to OpenAI and returns their IDs
*
* @private utility for `OpenAiAssistantExecutionTools` and `OpenAiCompatibleExecutionTools`
*/
async function uploadFilesToOpenAi(client, files) {
const fileIds = [];
for (const file of files) {
// Note: OpenAI API expects a File object or a ReadStream
// In browser environment, we can pass the File object directly
// In Node.js environment, we might need to convert it or use a different approach
// But since `Prompt.files` already contains `File` objects, we try to pass them directly
const uploadedFile = await client.files.create({
file: file,
purpose: 'assistants',
});
fileIds.push(uploadedFile.id);
}
return fileIds;
}
/**
* Execution Tools for calling OpenAI API Assistants
*
* This is useful for calling OpenAI API with a single assistant, for more wide usage use `OpenAiExecutionTools`.
*
* Note: [๐ฆ] There are several different things in Promptbook:
* - `Agent` - which represents an AI Agent with its source, memories, actions, etc. Agent is a higher-level abstraction which is internally using:
* - `LlmExecutionTools` - which wraps one or more LLM models and provides an interface to execute them
* - `AgentLlmExecutionTools` - which is a specific implementation of `LlmExecutionTools` that wraps another LlmExecutionTools and applies agent-specific system prompts and requirements
* - `OpenAiAssistantExecutionTools` - which is a specific implementation of `LlmExecutionTools` for OpenAI models with assistant capabilities, recommended for usage in `Agent` or `AgentLlmExecutionTools`
* - `RemoteAgent` - which is an `Agent` that connects to a Promptbook Agents Server
*
* @public exported from `@promptbook/openai`
*/
class OpenAiAssistantExecutionTools extends OpenAiExecutionTools {
/**
* Creates OpenAI Execution Tools.
*
* @param options which are relevant are directly passed to the OpenAI client
*/
constructor(options) {
var _a;
if (options.isProxied) {
throw new NotYetImplementedError(`Proxy mode is not yet implemented for OpenAI assistants`);
}
super(options);
this.isCreatingNewAssistantsAllowed = false;
this.assistantId = options.assistantId;
this.isCreatingNewAssistantsAllowed = (_a = options.isCreatingNewAssistantsAllowed) !== null && _a !== void 0 ? _a : false;
if (this.assistantId === null && !this.isCreatingNewAssistantsAllowed) {
throw new NotAllowed(`Assistant ID is null and creating new assistants is not allowed - this configuration does not make sense`);
}
// <- TODO: !!! `OpenAiAssistantExecutionToolsOptions` - Allow `assistantId: null` together with `isCreatingNewAssistantsAllowed: true`
// TODO: [๐ฑ] Make limiter same as in `OpenAiExecutionTools`
}
get title() {
return 'OpenAI Assistant';
}
get description() {
return 'Use single assistant provided by OpenAI';
}
/**
* Calls OpenAI API to use a chat model.
*/
async callChatModel(prompt) {
return this.callChatModelStream(prompt, () => { });
}
/**
* Calls OpenAI API to use a chat model with streaming.
*/
async callChatModelStream(prompt, onProgress) {
var _a, _b, _c, _d;
if (this.options.isVerbose) {
console.info('๐ฌ OpenAI callChatModel call', { prompt });
}
const { content, parameters, modelRequirements /*, format*/ } = prompt;
const client = await this.getClient();
// TODO: [โ] Use here more modelRequirements
if (modelRequirements.modelVariant !== 'CHAT') {
throw new PipelineExecutionError('Use callChatModel only for CHAT variant');
}
// TODO: [๐จโ๐จโ๐งโ๐ง] Remove:
for (const key of ['maxTokens', 'modelName', 'seed', 'temperature']) {
if (modelRequirements[key] !== undefined) {
throw new NotYetImplementedError(`In \`OpenAiAssistantExecutionTools\` you cannot specify \`${key}\``);
}
}
/*
TODO: [๐จโ๐จโ๐งโ๐ง] Implement all of this for Assistants
const modelName = modelRequirements.modelName || this.getDefaultChatModel().modelName;
const modelSettings = {
model: modelName,
temperature: modelRequirements.temperature,
// <- TODO: [๐] Use `seed` here AND/OR use is `isDeterministic` for entire execution tools
// <- Note: [๐ง]
} as OpenAI.Chat.Completions.CompletionCreateParamsNonStreaming; // <- TODO: Guard here types better
if (format === 'JSON') {
modelSettings.response_format = {
type: 'json_object',
};
}
*/
// <- TODO: [๐ธ] Not all models are compatible with JSON mode
// > 'response_format' of type 'json_object' is not supported with this model.
const rawPromptContent = templateParameters(content, {
...parameters,
modelName: 'assistant',
// <- [๐ง ] What is the best value here
});
// Build thread messages: include previous thread messages + current user message
const threadMessages = [];
// TODO: [๐น] Maybe this should not be here but in other place, look at commit 39d705e75e5bcf7a818c3af36bc13e1c8475c30c
// Add previous messages from thread (if any)
if ('thread' in prompt && Array.isArray(prompt.thread)) {
const previousMessages = prompt.thread.map((msg) => ({
role: (msg.sender === 'assistant' ? 'assistant' : 'user'),
content: msg.content,
}));
threadMessages.push(...previousMessages);
}
// Always add the current user message
const currentUserMessage = {
role: 'user',
content: rawPromptContent,
};
if ('files' in prompt && Array.isArray(prompt.files) && prompt.files.length > 0) {
const fileIds = await uploadFilesToOpenAi(client, prompt.files);
currentUserMessage.attachments = fileIds.map((fileId) => ({
file_id: fileId,
tools: [{ type: 'file_search' }, { type: 'code_interpreter' }],
}));
}
threadMessages.push(currentUserMessage);
// Check if tools are being used - if so, use non-streaming mode
const hasTools = modelRequirements.tools !== undefined && modelRequirements.tools.length > 0;
const start = $getCurrentDate();
let complete;
// [๐ฑโ๐] When tools are present, we need to use the non-streaming Runs API
// because streaming doesn't support tool execution flow properly
if (hasTools) {
onProgress({
content: '',
modelName: 'assistant',
timing: { start, complete: $getCurrentDate() },
usage: UNCERTAIN_USAGE,
rawPromptContent,
rawRequest: null,
rawResponse: null,
});
const rawRequest = {
assistant_id: this.assistantId,
thread: {
messages: threadMessages,
},
tools: mapToolsToOpenAi(modelRequirements.tools),
};
if (this.options.isVerbose) {
console.info(colors.bgWhite('rawRequest (non-streaming with tools)'), JSON.stringify(rawRequest, null, 4));
}
// Create thread and run
const threadAndRun = await client.beta.threads.createAndRun(rawRequest);
let run = threadAndRun;
const completedToolCalls = [];
const toolCallStartedAt = new Map();
// Poll until run completes or requires action
while (run.status === 'queued' || run.status === 'in_progress' || run.status === 'requires_action') {
if (run.status === 'requires_action' && ((_a = run.required_action) === null || _a === void 0 ? void 0 : _a.type) === 'submit_tool_outputs') {
// Execute tools
const toolCalls = run.required_action.submit_tool_outputs.tool_calls;
const toolOutputs = [];
for (const toolCall of toolCalls) {
if (toolCall.type === 'function') {
const functionName = toolCall.function.name;
const functionArgs = JSON.parse(toolCall.function.arguments);
const calledAt = $getCurrentDate();
if (toolCall.id) {
toolCallStartedAt.set(toolCall.id, calledAt);
}
onProgress({
content: '',
modelName: 'assistant',
timing: { start, complete: $getCurrentDate() },
usage: UNCERTAIN_USAGE,
rawPromptContent,
rawRequest: null,
rawResponse: null,
toolCalls: [
{
name: functionName,
arguments: toolCall.function.arguments,
result: '',
rawToolCall: toolCall,
createdAt: calledAt,
},
],
});
if (this.options.isVerbose) {
console.info(`๐ง Executing tool: ${functionName}`, functionArgs);
}
// Get execution tools for script execution
const executionTools = this.options
.executionTools;
if (!executionTools || !executionTools.script) {
throw new PipelineExecutionError(`Model requested tool '${functionName}' but no executionTools.script were provided in OpenAiAssistantExecutionTools options`);
}
// TODO: [DRY] Use some common tool caller (similar to OpenAiCompatibleExecutionTools)
const scriptTools = Array.isArray(executionTools.script)
? executionTools.script
: [executionTools.script];
let functionResponse;
let errors;
try {
const scriptTool = scriptTools[0]; // <- TODO: [๐ง ] Which script tool to use?
functionResponse = await scriptTool.execute({
scriptLanguage: 'javascript',
script: `
const args = ${JSON.stringify(functionArgs)};
return await ${functionName}(args);
`,
parameters: prompt.parameters,
});
if (this.options.isVerbose) {
console.info(`โ
Tool ${functionName} executed:`, functionResponse);
}
}
catch (error) {
assertsError(error);
const serializedError = serializeError(error);
errors = [serializedError];
functionResponse = spaceTrim$2((block) => `
The invoked tool \`${functionName}\` failed with error:
\`\`\`json
${block(JSON.stringify(serializedError, null, 4))}
\`\`\`
`);
console.error(colors.bgRed(`โ Error executing tool ${functionName}:`));
console.error(error);
}
toolOutputs.push({
tool_call_id: toolCall.id,
output: functionResponse,
});
completedToolCalls.push({
name: functionName,
arguments: toolCall.function.arguments,
result: functionResponse,
rawToolCall: toolCall,
createdAt: toolCall.id ? toolCallStartedAt.get(toolCall.id) || calledAt : calledAt,
errors,
});
}
}
// Submit tool outputs
run = await client.beta.threads.runs.submitToolOutputs(run.thread_id, run.id, {
tool_outputs: toolOutputs,
});
}
else {
// Wait a bit before polling again
await new Promise((resolve) => setTimeout(resolve, 500));
run = await client.beta.threads.runs.retrieve(run.thread_id, run.id);
}
}
if (run.status !== 'completed') {
throw new PipelineExecutionError(`Assistant run failed with status: ${run.status}`);
}
// Get messages from the thread
const messages = await client.beta.threads.messages.list(run.thread_id);
const assistantMessages = messages.data.filter((msg) => msg.role === 'assistant');
if (assistantMessages.length === 0) {
throw new PipelineExecutionError('No assistant messages found after run completion');
}
const lastMessage = assistantMessages[0];
const textContent = lastMessage.content.find((c) => c.type === 'text');
if (!textContent || textContent.type !== 'text') {
throw new PipelineExecutionError('No text content in assistant response');
}
complete = $getCurrentDate();
const resultContent = textContent.text.value;
const usage = UNCERTAIN_USAGE;
// Progress callback with final result
const finalChunk = {
content: resultContent,
modelName: 'assistant',
timing: { start, complete },
usage,
rawPromptContent,
rawRequest,
rawResponse: { run, messages: messages.data },
toolCalls: completedToolCalls.length > 0 ? completedToolCalls : undefined,
};
onProgress(finalChunk);
return exportJson({
name: 'promptResult',
message: `Result of \`OpenAiAssistantExecutionTools.callChatModelStream\` (with tools)`,
order: [],
value: finalChunk,
});
}
// Streaming mode (without tools)
const rawRequest = {
// TODO: [๐จโ๐จโ๐งโ๐ง] ...modelSettings,
// TODO: [๐จโ๐จโ๐งโ๐ง][๐ง ] What about system message for assistants, does it make sense - combination of OpenAI assistants with Promptbook Personas
assistant_id: this.assistantId,
thread: {
messages: threadMessages,
},
tools: modelRequirements.tools === undefined ? undefined : mapToolsToOpenAi(modelRequirements.tools),
// <- TODO: Add user identification here> user: this.options.user,
};
if (this.options.isVerbose) {
console.info(colors.bgWhite('rawRequest (streaming)'), JSON.stringify(rawRequest, null, 4));
}
const stream = await client.beta.threads.createAndRunStream(rawRequest);
stream.on('connect', () => {
if (this.options.isVerbose) {
console.info('connect', stream.currentEvent);
}
});
stream.on('textDelta', (textDelta, snapshot) => {
if (this.options.isVerbose && textDelta.value) {
console.info('textDelta', textDelta.value);
}
const chunk = {
content: snapshot.value,
modelName: 'assistant',
timing: {
start,
complete: $getCurrentDate(),
},
usage: UNCERTAIN_USAGE,
rawPromptContent,
rawRequest,
rawResponse: snapshot,
};
onProgress(chunk);
});
stream.on('messageCreated', (message) => {
if (this.options.isVerbose) {
console.info('messageCreated', message);
}
});
stream.on('messageDone', (message) => {
if (this.options.isVerbose) {
console.info('messageDone', message);
}
});
// TODO: [๐ฑโ๐] Handle tool calls in assistants
// Note: OpenAI Assistant streaming with tool calls requires special handling.
// The stream will pause when a tool call is needed, and we need to:
// 1. Wait for the run to reach 'requires_action' status
// 2. Execute the tool calls
// 3. Submit tool outputs via a separate API call (not on the stream)
// 4. Continue the run
// This requires switching to non-streaming mode or using the Runs API directly.
// For now, tools with assistants should use the non-streaming chat completions API instead.
const rawResponse = await stream.finalMessages();
if (this.options.isVerbose) {
console.info(colors.bgWhite('rawResponse'), JSON.stringify(rawResponse, null, 4));
}
if (rawResponse.length !== 1) {
throw new PipelineExecutionError(`There is NOT 1 BUT ${rawResponse.length} finalMessages from OpenAI`);
}
if (rawResponse[0].content.length !== 1) {
throw new PipelineExecutionError(`There is NOT 1 BUT ${rawResponse[0].content.length} finalMessages content from OpenAI`);
}
if (((_b = rawResponse[0].content[0]) === null || _b === void 0 ? void 0 : _b.type) !== 'text') {
throw new PipelineExecutionError(`There is NOT 'text' BUT ${(_c = rawResponse[0].content[0]) === null || _c === void 0 ? void 0 : _c.type} finalMessages content type from OpenAI`);
}
const resultContent = (_d = rawResponse[0].content[0]) === null || _d === void 0 ? void 0 : _d.text.value;
// <- TODO: [๐ง ] There are also annotations, maybe use them
// eslint-disable-next-line prefer-const
complete = $getCurrentDate();
const usage = UNCERTAIN_USAGE;
// <- TODO: [๐ฅ] Compute real usage for assistant
// ?> const usage = computeOpenAiUsage(content, resultContent || '', rawResponse);
if (resultContent === null) {
throw new PipelineExecutionError('No response message from OpenAI');
}
return exportJson({
name: 'promptResult',
message: `Result of \`OpenAiAssistantExecutionTools.callChatModelStream\``,
order: [],
value: {
content: resultContent,
modelName: 'assistant',
// <- TODO: [๐ฅ] Detect used model in assistant
// ?> model: rawResponse.model || modelName,
timing: {
start,
complete,
},
usage,
rawPromptContent,
rawRequest,
rawResponse,
// <- [๐ฏ]
},
});
}
/*
public async playground() {
const client = await this.getClient();
// List all assistants
const assistants = await client.beta.assistants.list();
// Get details of a specific assistant
const assistantId = 'asst_MO8fhZf4dGloCfXSHeLcIik0';
const assistant = await client.beta.assistants.retrieve(assistantId);
// Update an assistant
const updatedAssistant = await client.beta.assistants.update(assistantId, {
name: assistant.name + '(M)',
description: 'Updated description via Promptbook',
metadata: {
[Math.random().toString(36).substring(2, 15)]: new Date().toISOString(),
},
});
await forEver();
}
*/
/**
* Get an existing assistant tool wrapper
*/
getAssistant(assistantId) {
return new OpenAiAssistantExecutionTools({
...this.options,
assistantId,
});
}
async createNewAssistant(options) {
if (!this.isCreatingNewAssistantsAllowed) {
throw new NotAllowed(`Creating new assistants is not allowed. Set \`isCreatingNewAssistantsAllowed: true\` in options to enable this feature.`);
}
// await this.playground();
const { name, instructions, knowledgeSources, tools } = options;
const client = await this.getClient();
let vectorStoreId;
// If knowledge sources are provided, create a vector store with them
if (knowledgeSources && knowledgeSources.length > 0) {
if (this.options.isVerbose) {
console.info(`๐ Creating vector store with ${knowledgeSources.length} knowledge sources...`);
}
// Create a vector store
const vectorStore = await client.beta.vectorStores.create({
name: `${name} Knowledge Base`,
});
vectorStoreId = vectorStore.id;
if (this.options.isVerbose) {
console.info(`โ
Vector store created: ${vectorStoreId}`);
}
// Upload files from knowledge sources to the vector store
const fileStreams = [];
for (const source of knowledgeSources) {
try {
// Check if it's a URL
if (source.startsWith('http://') || source.startsWith('https://')) {
// Download the file
const response = await fetch(source);
if (!response.ok) {
console.error(`Failed to download ${source}: ${response.statusText}`);
continue;
}
const buffer = await response.arrayBuffer();
const filename = source.split('/').pop() || 'downloaded-file';
const blob = new Blob([buffer]);
const file = new File([blob], filename);
fileStreams.push(file);
}
else {
/*
TODO: [๐ฑโ๐] Resolve problem with browser environment
// Assume it's a local file path
// Note: This will work in Node.js environment
// For browser environments, this would need different handling
const fs = await import('fs');
const fileStream = fs.createReadStream(source);
fileStreams.push(fileStream);
*/
}
}
catch (error) {
console.error(`Error processing knowledge source ${source}:`, error);
}
}
// Batch upload files to the vector store
if (fileStreams.length > 0) {
try {
await client.beta.vectorStores.fileBatches.uploadAndPoll(vectorStoreId, {
files: fileStreams,
});
if (this.options.isVerbose) {
console.info(`โ
Uploaded ${fileStreams.length} files to vector store`);
}
}
catch (error) {
console.error('Error uploading files to vector store:', error);
}
}
}
// Create assistant with vector store attached
const assistantConfig = {
name,
description: 'Assistant created via Promptbook',
model: 'gpt-4o',
instructions,
tools: [
/* TODO: [๐ง ] Maybe add { type: 'code_interpreter' }, */
{ type: 'file_search' },
...(tools === undefined ? [] : mapToolsToOpenAi(tools)),
],
};
// Attach vector store if created
if (vectorStoreId) {
assistantConfig.tool_resources = {
file_search: {
vector_store_ids: [vectorStoreId],
},
};
}
const assistant = await client.beta.assistants.create(assistantConfig);
console.log(`โ
Assistant created: ${assistant.id}`);
// TODO: [๐ฑโ๐] Try listing existing assistants
// TODO: [๐ฑโ๐] Try marking existing assistants by DISCRIMINANT
// TODO: [๐ฑโ๐] Allow to update and reconnect to existing assistants
return new OpenAiAssistantExecutionTools({
...this.options,
isCreatingNewAssistantsAllowed: false,
assistantId: assistant.id,
});
}
async updateAssistant(options) {
if (!this.isCreatingNewAssistantsAllowed) {
throw new NotAllowed(`Updating assistants is not allowed. Set \`isCreatingNewAssistantsAllowed: true\` in options to enable this feature.`);
}
const { assistantId, name, instructions, knowledgeSources, tools } = options;
const client = await this.getClient();
let vectorStoreId;
// If knowledge sources are provided, create a vector store with them
// TODO: [๐ง ] Reuse vector store creation logic from createNewAssistant
if (knowledgeSources && knowledgeSources.length > 0) {
if (this.options.isVerbose) {
console.info(`๐ Creating vector store for update with ${knowledgeSources.length} knowledge sources...`);
}
// Create a vector store
const vectorStore = await client.beta.vectorStores.create({
name: `${name} Knowledge Base`,
});
vectorStoreId = vectorStore.id;
if (this.options.isVerbose) {
console.info(`โ
Vector store created: ${vectorStoreId}`);
}
// Upload files from knowledge sources to the vector store
const fileStreams = [];
for (const source of knowledgeSources) {
try {
// Check if it's a URL
if (source.startsWith('http://') || source.startsWith('https://')) {
// Download the file
const response = await fetch(source);
if (!response.ok) {
console.error(`Failed to download ${source}: ${response.statusText}`);
continue;
}
const buffer = await response.arrayBuffer();
const filename = source.split('/').pop() || 'downloaded-file';
const blob = new Blob([buffer]);
const file = new File([blob], filename);
fileStreams.push(file);
}
else {
/*
TODO: [๐ฑโ๐] Resolve problem with browser environment
// Assume it's a local file path
// Note: This will work in Node.js environment
// For browser environments, this would need different handling
const fs = await import('fs');
const fileStream = fs.createReadStream(source);
fileStreams.push(fileStream);
*/
}
}
catch (error) {
console.error(`Error processing knowledge source ${source}:`, error);
}
}
// Batch upload files to the vector store
if (fileStreams.length > 0) {
try {
await client.beta.vectorStores.fileBatches.uploadAndPoll(vectorStoreId, {
files: fileStreams,
});
if (this.options.isVerbose) {
console.info(`โ
Uploaded ${fileStreams.length} files to vector store`);
}
}
catch (error) {
console.error('Error uploading files to vector store:', error);
}
}
}
const assistantUpdate = {
name,
instructions,
tools: [
/* TODO: [๐ง ] Maybe add { type: 'code_interpreter' }, */
{ type: 'file_search' },
...(tools === undefined ? [] : mapToolsToOpenAi(tools)),
],
};
if (vectorStoreId) {
assistantUpdate.tool_resources = {
file_search: {
vector_store_ids: [vectorStoreId],
},
};
}
const assistant = await client.beta.assistants.update(assistantId, assistantUpdate);
if (this.options.isVerbose) {
console.log(`โ
Assistant updated: ${assistant.id}`);
}
return new OpenAiAssistantExecutionTools({
...this.options,
isCreatingNewAssistantsAllowed: false,
assistantId: assistant.id,
});
}
/**
* Discriminant for type guards
*/
get discriminant() {
return DISCRIMINANT;
}
/**
* Type guard to check if given `LlmExecutionTools` are instanceof `OpenAiAssistantExecutionTools`
*
* Note: This is useful when you can possibly have multiple versions of `@promptbook/openai` installed
*/
static isOpenAiAssistantExecutionTools(llmExecutionTools) {
return llmExecutionTools.discriminant === DISCRIMINANT;
}
}
/**
* Discriminant for type guards
*
* @private const of `OpenAiAssistantExecutionTools`
*/
const DISCRIMINANT = 'OPEN_AI_ASSISTANT_V1';
/**
* TODO: !!!!! [โจ๐ฅ] Knowledge should work both with and without scrapers
* TODO: [๐] In `OpenAiAssistantExecutionTools` Allow to create abstract assistants with `isCreatingNewAssistantsAllowed`
* TODO: [๐ง ][๐งโโ๏ธ] Maybe there can be some wizard for those who want to use just OpenAI
* TODO: Maybe make custom OpenAiError
* TODO: [๐ง ][๐] Maybe use `isDeterministic` from options
* TODO: [๐ง ][๐ฐ] Allow to pass `title` for tracking purposes
*/
/**
* Execution Tools for calling OpenAI API
*
* @public exported from `@promptbook/openai`
*/
const createOpenAiAssistantExecutionTools = Object.assign((options) => {
// TODO: [๐ง ][main] !!4 If browser, auto add `dangerouslyAllowBrowser`
if (($isRunningInBrowser() || $isRunningInWebWorker()) && !options.dangerouslyAllowBrowser) {
options = { ...options, dangerouslyAllowBrowser: true };
}
return new OpenAiAssistantExecutionTools(options);
}, {
packageName: '@promptbook/openai',
className: 'OpenAiAssistantExecutionTools',
});
/**
* TODO: [๐ฆบ] Is there some way how to put `packageName` and `className` on top and function definition on bottom?
* TODO: [๐ถ] Naming "constructor" vs "creator" vs "factory"
*/
/**
* 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$2((block) => `
Remote server requires root url \`/\`
You have provided \`remoteServerUrl\`:
${block(remoteServerUrl)}
But something like this is expected:
${block(remoteServerUrlParsed.href)}
Note: If you need to run multiple services on the same server, use 3rd or 4th degree subdomain
`));
}
return new Promise((resolve, reject) => {
const socket = io(remoteServerUrl, {
retries: CONNECTION_RETRIES_LIMIT,
timeout: CONNECTION_TIMEOUT_MS,
path: '/socket.io',
transports: ['polling', 'websocket' /*, <- TODO: [๐ฌ] Allow to pass `transports`, add 'webtransport' */],
});
// console.log('Connecting to', this.options.remoteServerUrl.href, { socket });
socket.on('connect', () => {
resolve(socket);
});
// TODO: [๐ฉ] Better timeout handling
setTimeout(() => {
reject(new Error(`Timeout while connecting to ${remoteServerUrl}`));
}, CONNECTION_TIMEOUT_MS);
});
}
/**
* 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`
*/
/**
* Execution Tools for calling OpenAI compatible API
*
* Note: This can be used for any OpenAI compatible APIs
*
* @public exported from `@promptbook/openai`
*/
const createOpenAiCompatibleExecutionTools = Object.assign((options) => {
if (options.isProxied) {
return new RemoteLlmExecutionTools({
...options,
identification: {
isAnonymous: true,
llmToolsConfiguration: [
{
title: 'OpenAI Compatible (proxied)',
packageName: '@promptbook/openai',
className: 'OpenAiCompatibleExecutionTools',
options: {
...options,
isProxied: false,
},
},
],
},
});
}
if (($isRunningInBrowser() || $isRunningInWebWorker()) && !options.dangerouslyAllowBrowser) {
options = { ...options, dangerouslyAllowBrowser: true };
}
return new HardcodedOpenAiCompatibleExecutionTools(options.defaultModelName, options);
}, {
packageName: '@promptbook/openai',
className: 'OpenAiCompatibleExecutionTools',
});
/**
* Execution Tools for calling ONE SPECIFIC PRECONFIGURED OpenAI compatible provider
*
* @private for `createOpenAiCompatibleExecutionTools`
*/
class HardcodedOpenAiCompatibleExecutionTools extends OpenAiCompatibleExecutionTools {
/**
* Creates OpenAI compatible Execution Tools.
*
* @param options which are relevant are directly passed to the OpenAI compatible client
*/
constructor(defaultModelName, options) {
super(options);
this.defaultModelName = defaultModelName;
this.options = options;
}
get title() {
return `${this.defaultModelName} on ${this.options.baseURL}`;
}
get description() {
return `OpenAI compatible connected to "${this.options.baseURL}" model "${this.defaultModelName}"`;
}
/**
* List all available models (non dynamically)
*
* Note: Purpose of this is to provide more information about models than standard listing from API
*/
get HARDCODED_MODELS() {
return [
{
modelName: this.defaultModelName,
modelVariant: 'CHAT',
modelDescription: '', // <- TODO: What is the best value here, maybe `this.description`?
},
];
}
/**
* Computes the usage
*/
computeUsage(...args) {
return {
...computeOpenAiUsage(...args),
price: UNCERTAIN_ZERO_VALUE, // <- TODO: Maybe in future pass this counting mechanism, but for now, we dont know
};
}
/**
* Default model for chat variant.
*/
getDefaultChatModel() {
return this.getDefaultModel(this.defaultModelName);
}
/**
* Default model for completion variant.
*/
getDefaultCompletionModel() {
throw new PipelineExecutionError(`${this.title} does not support COMPLETION model variant`);
}
/**
* Default model for completion variant.
*/
getDefaultEmbeddingModel() {
throw new PipelineExecutionError(`${this.title} does not support EMBEDDING model variant`);
}
/**
* Default model for completion variant.
*/
getDefaultImageGenerationModel() {
throw new PipelineExecutionError(`${this.title} does not support IMAGE_GENERATION model variant`);
}
}
/**
* TODO: [๐ฆบ] Is there some way how to put `packageName` and `className` on top and function definition on bottom?
* TODO: [๐ถ] Naming "constructor" vs "creator" vs "factory"
*/
/**
* Execution Tools for calling OpenAI API
*
* Note: This can be also used for other OpenAI compatible APIs, like Ollama
*
* @public exported from `@promptbook/openai`
*/
const createOpenAiExecutionTools = Object.assign((options) => {
if (($isRunningInBrowser() || $isRunningInWebWorker()) && !options.dangerouslyAllowBrowser) {
options = { ...options, dangerouslyAllowBrowser: true };
}
if (options.isProxied) {
throw new NotYetImplementedError(`Proxy mode is not yet implemented in createOpenAiExecutionTools`);
}
return new OpenAiExecutionTools(options);
}, {
packageName: '@promptbook/openai',
className: 'OpenAiExecutionTools',
});
/**
* TODO: [๐ฆบ] Is there some way how to put `packageName` and `className` on top and function definition on bottom?
* TODO: [๐ถ] Naming "constructor" vs "creator" vs "factory"
*/
/**
* Safely retrieves the global scope object (window in browser, global in Node.js)
* regardless of the JavaScript environment in which the code is running
*
* Note: `$` is used to indicate that this function is not a pure function - it access global scope
*
* @private internal function of `$Register`
*/
function $getGlobalScope() {
return Function('return this')();
}
/**
* Global registry for storing and managing registered entities of a given type.
*
* Note: `$` is used to indicate that this function is not a pure function - it accesses and adds variables in global scope.
*
* @private internal utility, exported are only singleton instances of this class
*/
class $Register {
constructor(registerName) {
this.registerName = registerName;
const storageName = `_promptbook_${normalizeTo_snake_case(registerName)}`;
const globalScope = $getGlobalScope();
if (globalScope[storageName] === undefined) {
globalScope[storageName] = [];
}
else if (!Array.isArray(globalScope[storageName])) {
throw new UnexpectedError(`Expected (global) ${storageName} to be an array, but got ${typeof globalScope[storageName]}`);
}
this.storage = globalScope[storageName];
}
list() {
// <- TODO: ReadonlyDeep<ReadonlyArray<TRegistered>>
return this.storage;
}
register(registered) {
const { packageName, className } = registered;
const existingRegistrationIndex = this.storage.findIndex((item) => item.packageName === packageName && item.className === className);
const existingRegistration = this.storage[existingRegistrationIndex];
if (!existingRegistration) {
this.storage.push(registered);
}
else {
this.storage[existingRegistrationIndex] = registered;
}
return {
registerName: this.registerName,
packageName,
className,
get isDestroyed() {
return false;
},
destroy() {
throw new NotYetImplementedError(`Registration to ${this.registerName} is permanent in this version of Promptbook`);
},
};
}
}
/**
* Register for LLM tools.
*
* Note: `$` is used to indicate that this interacts with the global scope
* @singleton Only one instance of each register is created per build, but there can be more instances across different builds or environments.
* @public exported from `@promptbook/core`
*/
const $llmToolsRegister = new $Register('llm_execution_tools_constructors');
/**
* TODO: [ยฎ] DRY Register logic
*/
// Note: OpenAiCompatibleExecutionTools is an abstract class and cannot be instantiated directly
/**
* Registration of LLM provider
*
* Warning: This is not useful for the end user, it is just a side effect of the mechanism that handles all available LLM tools
*
* @public exported from `@promptbook/openai`
* @public exported from `@promptbook/wizard`
* @public exported from `@promptbook/cli`
*/
const _OpenAiRegistration = $llmToolsRegister.register(createOpenAiExecutionTools);
/**
* Registration of the OpenAI Assistant provider
*
* Note: [๐] Configurations registrations are done in register-constructor.ts BUT constructor register-constructor.ts
*
* @public exported from `@promptbook/openai`
* @public exported from `@promptbook/wizard`
* @public exported from `@promptbook/cli`
*/
const _OpenAiAssistantRegistration = $llmToolsRegister.register(createOpenAiAssistantExecutionTools);
/**
* Registration of the OpenAI Compatible provider
*
* Note: [๐] Configurations registrations are done in register-constructor.ts BUT constructor register-constructor.ts
*
* @public exported from `@promptbook/openai`
* @public exported from `@promptbook/wizard`
* @public exported from `@promptbook/cli`
*/
const _OpenAiCompatibleRegistration = $llmToolsRegister.register(createOpenAiCompatibleExecutionTools);
/**
* Note: OpenAiCompatibleExecutionTools is an abstract class and cannot be registered directly.
* It serves as a base class for OpenAiExecutionTools and other compatible implementations.
*/
/**
* TODO: [๐ถ] Naming "constructor" vs "creator" vs "factory"
* Note: [๐] Ignore a discrepancy between file name and entity name
*/
export { BOOK_LANGUAGE_VERSION, OPENAI_MODELS, OpenAiAssistantExecutionTools, OpenAiCompatibleExecutionTools, OpenAiExecutionTools, PROMPTBOOK_ENGINE_VERSION, _OpenAiAssistantRegistration, _OpenAiCompatibleRegistration, _OpenAiRegistration, createOpenAiAssistantExecutionTools, createOpenAiCompatibleExecutionTools, createOpenAiExecutionTools };
//# sourceMappingURL=index.es.js.map