@promptbook/remote-server
Version:
Promptbook: Create persistent AI agents that turn your company's scattered knowledge into action
1,613 lines (1,574 loc) โข 1.55 MB
JavaScript
import { join, basename, dirname, isAbsolute } from 'path';
import { spawn } from 'child_process';
import colors from 'colors';
import _spaceTrim, { spaceTrim as spaceTrim$1 } from 'spacetrim';
import { forTime } from 'waitasecond';
import http from 'http';
import express from 'express';
import { Server } from 'socket.io';
import { stat, access, constants, readFile, writeFile, readdir, mkdir, watch, rm } from 'fs/promises';
import { Readability } from '@mozilla/readability';
import { JSDOM } from 'jsdom';
import { SHA256 } from 'crypto-js';
import hexEncoder from 'crypto-js/enc-hex';
import { Converter } from 'showdown';
import * as OpenApiValidator from 'express-openapi-validator';
import swaggerUi from 'swagger-ui-express';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { Subject, BehaviorSubject } from 'rxjs';
import { randomBytes } from 'crypto';
import moment from 'moment';
import { lookup, extension } from 'mime-types';
import sha256 from 'crypto-js/sha256';
import { parse, unparse } from 'papaparse';
import { fileSearchTool, tool, Agent as Agent$1, webSearchTool, run, setDefaultOpenAIClient, setDefaultOpenAIKey } from '@openai/agents';
import Bottleneck from 'bottleneck';
import OpenAI from 'openai';
// โ ๏ธ 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.112.0-73';
/**
* TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
* Note: [๐] Ignore a discrepancy between file name and entity name
*/
/**
* Trims string from all 4 sides
*
* Note: This is a re-exported function from the `spacetrim` package which is
* Developed by same author @hejny as this package
*
* @see https://github.com/hejny/spacetrim#usage
*
* @public exported from `@promptbook/utils`
*/
const spaceTrim = spaceTrim$1;
/**
* Class implementing take chain.
*
* @de
*
* @private util of `@promptbook/color`
*/
class TakeChain {
constructor(value) {
this.value = value;
}
then(callback) {
const newValue = callback(this.value);
return take(newValue);
}
}
/**
* A function that takes an initial value and returns a proxy object with chainable methods.
*
* @param {*} initialValue - The initial value.
* @returns {Proxy<WithTake<TValue>>} - A proxy object with a `take` method.
* @deprecated [๐คก] Use some better functional library instead of `TakeChain`
*
* @private util of `@promptbook/color`
*/
function take(initialValue) {
if (initialValue instanceof TakeChain) {
return initialValue;
}
return new Proxy(new TakeChain(initialValue), {
get(target, property, receiver) {
if (Reflect.has(target, property)) {
return Reflect.get(target, property, receiver);
}
else if (Reflect.has(initialValue, property)) {
return Reflect.get(initialValue, property, receiver);
}
else {
return undefined;
}
},
});
}
/**
* ๐จ 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}`);
}
}
/**
* Constant for short hex lengths.
*/
const SHORT_HEX_LENGTHS = new Set([3, 4]);
/**
* Constant for long hex lengths.
*/
const LONG_HEX_LENGTHS = new Set([6, 8]);
/**
* Parses a hex string into RGBA channel values.
*
* @param hex - Hex value such as `#09d`, `009edd`, `#009eddff`.
* @returns RGBA channel values.
*
* @private function of Color
*/
function parseHexColor(hex) {
const sanitized = hex.startsWith('#') ? hex.substring(1) : hex;
const throwInvalidHex = () => {
throw new Error(`Can not parse color from hex string "${hex}"`);
};
if (SHORT_HEX_LENGTHS.has(sanitized.length)) {
return {
red: parseShortHexChannel(sanitized.charAt(0), throwInvalidHex),
green: parseShortHexChannel(sanitized.charAt(1), throwInvalidHex),
blue: parseShortHexChannel(sanitized.charAt(2), throwInvalidHex),
alpha: sanitized.length === 4 ? parseShortHexChannel(sanitized.charAt(3), throwInvalidHex) : 255,
};
}
if (LONG_HEX_LENGTHS.has(sanitized.length)) {
return {
red: parseLongHexChannel(sanitized, 0, throwInvalidHex),
green: parseLongHexChannel(sanitized, 2, throwInvalidHex),
blue: parseLongHexChannel(sanitized, 4, throwInvalidHex),
alpha: sanitized.length === 8 ? parseLongHexChannel(sanitized, 6, throwInvalidHex) : 255,
};
}
return throwInvalidHex();
}
/**
* Parses short hex channel.
*/
function parseShortHexChannel(char, onError) {
if (!char) {
return onError();
}
const parsed = parseInt(char, 16);
if (Number.isNaN(parsed)) {
return onError();
}
return parsed * 16;
}
/**
* Parses long hex channel.
*/
function parseLongHexChannel(hex, start, onError) {
const segment = hex.substr(start, 2);
if (segment.length < 2) {
return onError();
}
const parsed = parseInt(segment, 16);
if (Number.isNaN(parsed)) {
return onError();
}
return parsed;
}
/**
* Pattern matching hsl.
*/
const HSL_REGEX = /^hsl\(\s*([0-9.]+)\s*,\s*([0-9.]+)%\s*,\s*([0-9.]+)%\s*\)$/;
/**
* Parses an HSL string into RGBA channel values.
*
* @param hsl - HSL string such as `hsl(197.1, 100%, 43.3%)`.
* @returns RGBA channel values.
*
* @private function of Color
*/
function parseHslColor(hsl) {
const match = hsl.match(HSL_REGEX);
if (!match) {
throw new Error(`Invalid hsl string format: "${hsl}"`);
}
const hue = parseFloat(match[1]);
const saturation = parseFloat(match[2]) / 100;
const lightness = parseFloat(match[3]) / 100;
const { red, green, blue } = convertHslToRgb(hue, saturation, lightness);
return {
red,
green,
blue,
alpha: 255,
};
}
/**
* Handles convert hsl to Rgb.
*/
function convertHslToRgb(h, s, l) {
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
const m = l - c / 2;
let r1 = 0;
let g1 = 0;
let b1 = 0;
if (h >= 0 && h < 60) {
r1 = c;
g1 = x;
}
else if (h >= 60 && h < 120) {
r1 = x;
g1 = c;
}
else if (h >= 120 && h < 180) {
g1 = c;
b1 = x;
}
else if (h >= 180 && h < 240) {
g1 = x;
b1 = c;
}
else if (h >= 240 && h < 300) {
r1 = x;
b1 = c;
}
else if (h >= 300 && h < 360) {
r1 = c;
b1 = x;
}
return {
red: Math.round((r1 + m) * 255),
green: Math.round((g1 + m) * 255),
blue: Math.round((b1 + m) * 255),
};
}
/**
* Pattern matching RGB.
*/
const RGB_REGEX = /^rgb\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/;
/**
* Pattern matching rgba.
*/
const RGBA_REGEX = /^rgba\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/;
/**
* Parses an RGB string into RGBA channel values.
*
* @param rgb - RGB string such as `rgb(0%, 62%, 86.7%)`.
* @returns RGBA channel values.
*
* @private function of Color
*/
function parseRgbColor(rgb) {
const match = rgb.match(RGB_REGEX);
if (!match) {
throw new Error(`Invalid rgb string format: "${rgb}"`);
}
return {
red: parseChannelValue(match[1]),
green: parseChannelValue(match[2]),
blue: parseChannelValue(match[3]),
alpha: 255,
};
}
/**
* Parses an RGBA string into RGBA channel values.
*
* @param rgba - RGBA string such as `rgba(0, 158, 221, 0.5)`.
* @returns RGBA channel values.
*
* @private function of Color
*/
function parseRgbaColor(rgba) {
const match = rgba.match(RGBA_REGEX);
if (!match) {
throw new Error(`Invalid rgba string format: "${rgba}"`);
}
return {
red: parseChannelValue(match[1]),
green: parseChannelValue(match[2]),
blue: parseChannelValue(match[3]),
alpha: parseAlphaValue(match[4]),
};
}
/**
* Parses channel value.
*/
function parseChannelValue(value) {
if (value.endsWith('%')) {
const percent = parseFloat(value);
return Math.round((percent / 100) * 255);
}
return Math.round(parseFloat(value));
}
/**
* Parses alpha value.
*/
function parseAlphaValue(value) {
if (value.endsWith('%')) {
const percent = parseFloat(value);
return Math.round((percent / 100) * 255);
}
const parsed = parseFloat(value);
if (parsed <= 1) {
return Math.round(parsed * 255);
}
return Math.round(parsed);
}
/**
* Pattern matching hsl regex.
*/
const HSL_REGEX_PATTERN = /^hsl\(\s*([0-9.]+)\s*,\s*([0-9.]+)%\s*,\s*([0-9.]+)%\s*\)$/;
/**
* Pattern matching RGB regex.
*/
const RGB_REGEX_PATTERN = /^rgb\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/;
/**
* Pattern matching rgba regex.
*/
const RGBA_REGEX_PATTERN = /^rgba\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/;
/**
* 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) {
const trimmed = color.trim();
if (CSS_COLORS[trimmed]) {
return Color.fromString(CSS_COLORS[trimmed]);
}
else if (Color.isHexColorString(trimmed)) {
return Color.fromHex(trimmed);
}
if (HSL_REGEX_PATTERN.test(trimmed)) {
return Color.fromHsl(trimmed);
}
else if (RGB_REGEX_PATTERN.test(trimmed)) {
return Color.fromRgbString(trimmed);
}
else if (RGBA_REGEX_PATTERN.test(trimmed)) {
return Color.fromRgbaString(trimmed);
}
else {
throw new Error(`Can not create a new Color instance from string "${trimmed}".`);
}
}
/**
* 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 { red, green, blue, alpha } = parseHexColor(hex);
return take(new Color(red, green, blue, alpha));
}
/**
* 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 { red, green, blue, alpha } = parseHslColor(hsl);
return take(new Color(red, green, blue, alpha));
}
/**
* 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 { red, green, blue, alpha } = parseRgbColor(rgb);
return take(new Color(red, green, blue, alpha));
}
/**
* 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 { red, green, blue, alpha } = parseRgbaColor(rgba);
return take(new Color(red, green, blue, alpha));
}
/**
* 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`);
}
}
/**
* Makes color transformer which returns a grayscale version of the color
*
* @param amount from 0 to 1
*
* @public exported from `@promptbook/color`
*/
function grayscale(amount) {
return ({ red, green, blue, alpha }) => {
const average = (red + green + blue) / 3;
red = Math.round(average * amount + red * (1 - amount));
green = Math.round(average * amount + green * (1 - amount));
blue = Math.round(average * amount + blue * (1 - amount));
return Color.fromValues(red, green, blue, alpha);
};
}
/**
* Converts HSL values to RGB values
*
* @param hue [0-1]
* @param saturation [0-1]
* @param lightness [0-1]
* @returns [red, green, blue] [0-255]
*
* @private util of `@promptbook/color`
*/
function hslToRgb(hue, saturation, lightness) {
let red;
let green;
let blue;
if (saturation === 0) {
// achromatic
red = lightness;
green = lightness;
blue = lightness;
}
else {
// TODO: Extract to separate function
const hue2rgb = (p, q, t) => {
if (t < 0)
t += 1;
if (t > 1)
t -= 1;
if (t < 1 / 6)
return p + (q - p) * 6 * t;
if (t < 1 / 2)
return q;
if (t < 2 / 3)
return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = lightness < 0.5 ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation;
const p = 2 * lightness - q;
red = hue2rgb(p, q, hue + 1 / 3);
green = hue2rgb(p, q, hue);
blue = hue2rgb(p, q, hue - 1 / 3);
}
return [Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255)];
}
// TODO: Properly name all used internal variables
/**
* Converts RGB values to HSL values
*
* @param red [0-255]
* @param green [0-255]
* @param blue [0-255]
* @returns [hue, saturation, lightness] [0-1]
*
* @private util of `@promptbook/color`
*/
function rgbToHsl(red, green, blue) {
red /= 255;
green /= 255;
blue /= 255;
const max = Math.max(red, green, blue);
const min = Math.min(red, green, blue);
let hue;
let saturation;
const lightness = (max + min) / 2;
if (max === min) {
// achromatic
hue = 0;
saturation = 0;
}
else {
const d = max - min;
saturation = lightness > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case red:
hue = (green - blue) / d + (green < blue ? 6 : 0);
break;
case green:
hue = (blue - red) / d + 2;
break;
case blue:
hue = (red - green) / d + 4;
break;
default:
hue = 0;
}
hue /= 6;
}
return [hue, saturation, lightness];
}
// TODO: Properly name all used internal variables
/**
* Makes color transformer which lighten the given color
*
* @param amount from 0 to 1
*
* @public exported from `@promptbook/color`
*/
function lighten(amount) {
return ({ red, green, blue, alpha }) => {
const [h, s, lInitial] = rgbToHsl(red, green, blue);
let l = lInitial + amount;
l = Math.max(0, Math.min(l, 1)); // Replace lodash clamp with Math.max and Math.min
const [r, g, b] = hslToRgb(h, s, l);
return Color.fromValues(r, g, b, alpha);
};
}
// TODO: Maybe implement by mix+hsl
/**
* Makes color transformer which saturate the given color
*
* @param amount from -1 to 1
*
* @public exported from `@promptbook/color`
*/
function saturate(amount) {
return ({ red, green, blue, alpha }) => {
const [h, sInitial, l] = rgbToHsl(red, green, blue);
let s = sInitial + amount;
s = Math.max(0, Math.min(s, 1));
const [r, g, b] = hslToRgb(h, s, l);
return Color.fromValues(r, g, b, alpha);
};
}
// TODO: Maybe implement by mix+hsl
/**
* Stable root directory used for Promptbook-owned temporary files and caches.
*
* @private internal utility for Promptbook temporary folders
*/
const PROMPTBOOK_TEMPORARY_DIRECTORY = '.promptbook';
/**
* Builds one normalized project-relative path inside Promptbook's dedicated temporary root.
*
* The returned path intentionally uses `/` separators so the same helper can be reused from
* Node.js and edge-safe code without depending on the Node `path` module.
*
* @private internal utility for Promptbook temporary folders
*/
function getPromptbookTemporaryPath(...pathSegments) {
const normalizedPathSegments = pathSegments.flatMap(splitPathSegments).filter(Boolean);
return [PROMPTBOOK_TEMPORARY_DIRECTORY, ...normalizedPathSegments].join('/');
}
/**
* Normalizes one raw path segment into slash-delimited pieces without empty items.
*/
function splitPathSegments(pathSegment) {
return pathSegment
.split(/[\\/]+/u)
.map((segment) => segment.trim())
.filter(Boolean);
}
// Note: [๐] Ignore a discrepancy between file name and entity name
/**
* Returns the same value that is passed as argument.
* No side effects.
*
* Note: It can be useful for:
*
* 1) Leveling indentation
* 2) Putting always-true or always-false conditions without getting eslint errors
*
* @param value any values
* @returns the same values
*
* @private within the repository
*/
function just(value) {
if (value === undefined) {
return undefined;
}
return value;
}
/**
* Name for the Promptbook
*
* TODO: [๐ฝ] Unite branding and make single place for it
*
* @public exported from `@promptbook/core`
*/
const NAME = `Promptbook`;
/**
* Email of the responsible person
*
* @public exported from `@promptbook/core`
*/
const ADMIN_EMAIL = 'pavol@ptbk.io';
/**
* Name of the responsible person for the Promptbook on GitHub
*
* @public exported from `@promptbook/core`
*/
const ADMIN_GITHUB_NAME = 'hejny';
/**
* Claim for the Promptbook
*
* TODO: [๐ฝ] Unite branding and make single place for it
*
* @public exported from `@promptbook/core`
*/
const CLAIM = `Create persistent AI agents that turn your company's scattered knowledge into action`;
// <- TODO: [๐] Pick the best claim
/**
* Color of the Promptbook
*
* TODO: [๐ฝ] Unite branding and make single place for it
*
* @public exported from `@promptbook/core`
*/
const PROMPTBOOK_COLOR = Color.fromString('promptbook');
// <- TODO: [๐ง ][๐ต] Using `Color` here increases the package size approx 3kb, maybe remove it
/**
* Colors for syntax highlighting in the `<BookEditor/>`
*
* TODO: [๐ฝ] Unite branding and make single place for it
*
* @public exported from `@promptbook/core`
*/
({
TITLE: Color.fromHex('#244EA8'),
LINE: Color.fromHex('#eeeeee'),
SEPARATOR: Color.fromHex('#cccccc'),
COMMITMENT: Color.fromHex('#DA0F78'),
NOTE_COMMITMENT: Color.fromHex('#8080807e'),
TODO_COMMITMENT_TEXT: Color.fromHex('#000000'),
TODO_COMMITMENT_BACKGROUND: Color.fromHex('#FFEB3B'),
PARAMETER: Color.fromHex('#8e44ad'),
CODE_BLOCK: Color.fromHex('#7700ffff'),
});
// <- TODO: [๐ง ][๐ต] Using `Color` here increases the package size approx 3kb, maybe remove it
/**
* Chat color of the Promptbook (in chat)
*
* TODO: [๐ฝ] Unite branding and make single place for it
*
* @public exported from `@promptbook/core`
*/
PROMPTBOOK_COLOR.then(lighten(0.1)).then(saturate(0.9)).then(grayscale(0.9));
// <- TODO: [๐ง ][๐ต] Using `Color` and `lighten`, `saturate`,... here increases the package size approx 3kb, maybe remove it
/**
* Color of the user (in chat)
*
* TODO: [๐ฝ] Unite branding and make single place for it
*
* @public exported from `@promptbook/core`
*/
Color.fromHex('#1D4ED8');
// <- TODO: [๐ง ][๐ต] Using `Color` here increases the package size approx 3kb, maybe remove it
/**
* When the title is not provided, the default title is used
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_BOOK_TITLE = `๐ Untitled agent`;
/**
* Maximum file size limit
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
/**
* Threshold value that determines when a dataset is considered "big"
* and may require special handling or optimizations
*
* For example, when error occurs in one item of the big dataset, it will not fail the whole pipeline
*
* @public exported from `@promptbook/core`
*/
const BIG_DATASET_TRESHOLD = 50;
/**
* Placeholder text used to represent a placeholder value of failed operation
*
* @public exported from `@promptbook/core`
*/
const FAILED_VALUE_PLACEHOLDER = '!?';
// <- 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;
// <- TODO: [โณ] Standardize timeouts, Make DEFAULT_TIMEOUT_MS as global constant
/**
* How many times to retry the connections
*
* @private within the repository - too low-level in comparison with other `MAX_...`
*/
const CONNECTION_RETRIES_LIMIT = 5;
/**
* Short time interval to prevent race conditions in milliseconds
*
* @private within the repository - too low-level in comparison with other `MAX_...`
*/
const IMMEDIATE_TIME = 10;
/**
* The maximum length of the (generated) filename
*
* @public exported from `@promptbook/core`
*/
const MAX_FILENAME_LENGTH = 30;
/**
* Strategy for caching the intermediate results for knowledge sources
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_INTERMEDIATE_FILES_STRATEGY = 'HIDE_AND_KEEP';
// <- TODO: [๐ก] Change to 'VISIBLE'
/**
* The maximum number of (LLM) tasks running in parallel
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_MAX_PARALLEL_COUNT = 5; // <- TODO: [๐คนโโ๏ธ]
/**
* The maximum number of attempts to execute LLM task before giving up
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_MAX_EXECUTION_ATTEMPTS = 7; // <- TODO: [๐คนโโ๏ธ]
// <- TODO: [๐] Make also `AGENTS_DIRNAME_ALTERNATIVES`
/**
* Where to store the temporary downloads
*
* Note: When the folder does not exist, it is created recursively
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_DOWNLOAD_CACHE_DIRNAME = `./${getPromptbookTemporaryPath('download-cache')}`;
/**
* Where to store the cache of executions for promptbook CLI
*
* Note: When the folder does not exist, it is created recursively
*
* @public exported from `@promptbook/core`
*/
`./${getPromptbookTemporaryPath('execution-cache')}`;
/**
* Where to store the scrape cache
*
* Note: When the folder does not exist, it is created recursively
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_SCRAPE_CACHE_DIRNAME = `./${getPromptbookTemporaryPath('scrape-cache')}`;
// <- TODO: [๐งโโ๏ธ]
/**
* Default settings for parsing and generating CSV files in Promptbook.
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_CSV_SETTINGS = Object.freeze({
delimiter: ',',
quoteChar: '"',
newline: '\n',
skipEmptyLines: true,
});
/**
* Controls whether verbose logging is enabled by default throughout the application.
*
* @public exported from `@promptbook/core`
*/
let DEFAULT_IS_VERBOSE = false;
/**
* Controls whether auto-installation of dependencies is enabled by default.
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_IS_AUTO_INSTALLED = false;
/**
* Default simulated duration for a task in milliseconds (used for progress reporting)
*
* @public exported from `@promptbook/core`
*/
const DEFAULT_TASK_SIMULATED_DURATION_MS = 5 * 60 * 1000; // 5 minutes
/**
* 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');
/**
* Indicates whether pipeline logic validation is enabled. When true, the pipeline logic is checked for consistency.
*
* @private within the repository
*/
const IS_PIPELINE_LOGIC_VALIDATED = just(
/**/
// Note: In normal situations, we check the pipeline logic:
true);
/**
* Note: [๐] Ignore a discrepancy between file name and entity name
* TODO: [๐ง ][๐งโโ๏ธ] Maybe join remoteServerUrl and path into single value
*/
/**
* 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);
}
}
/**
* Detects if the code is running in a Node.js environment
*
* 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 $isRunningInNode() {
try {
return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
}
catch (e) {
return false;
}
}
// TODO: [๐บ]
/**
* Normalize options for `execCommand` and `execCommands`
*
* Note: `$` is used to indicate that this function behaves differently according to `process.platform`
*
* @private internal utility of `execCommand` and `execCommands`
*/
function $execCommandNormalizeOptions(options) {
var _a, _b, _c, _d;
let command;
let cwd;
let crashOnError;
let args = [];
let timeout;
let isVerbose;
let env;
if (typeof options === 'string') {
// TODO: [1] DRY default values
command = options;
cwd = process.cwd();
crashOnError = true;
timeout = Infinity; // <- TODO: [โณ]
isVerbose = DEFAULT_IS_VERBOSE;
env = undefined;
}
else {
/*
TODO:
if ((options as any).commands !== undefined) {
commands = (options as any).commands;
} else {
commands = [(options as any).command];
}
*/
// TODO: [1] DRY default values
command = options.command;
cwd = (_a = options.cwd) !== null && _a !== void 0 ? _a : process.cwd();
crashOnError = (_b = options.crashOnError) !== null && _b !== void 0 ? _b : true;
timeout = (_c = options.timeout) !== null && _c !== void 0 ? _c : Infinity;
isVerbose = (_d = options.isVerbose) !== null && _d !== void 0 ? _d : DEFAULT_IS_VERBOSE;
env = options.env;
}
// TODO: /(-[a-zA-Z0-9-]+\s+[^\s]*)|[^\s]*/g
const _ = Array.from(command.matchAll(/(".*")|([^\s]*)/g))
.map(([match]) => match)
.filter((arg) => arg !== '');
if (_.length > 1) {
[command, ...args] = _;
}
if (options.args) {
args = [...args, ...options.args];
}
let humanReadableCommand = !['npx', 'npm'].includes(command) ? command : args[0];
if (['ts-node'].includes(humanReadableCommand)) {
humanReadableCommand += ` ${args[1]}`;
}
if (/^win/.test(process.platform) && ['npm', 'npx'].includes(command)) {
command = `${command}.cmd`;
}
return { command, humanReadableCommand, args, cwd, crashOnError, timeout, isVerbose, env };
}
// TODO: This should show type error> execCommandNormalizeOptions({ command: '', commands: [''] });
/**
* Run one command in a shell
*
* Note: There are 2 similar functions in the codebase:
* - `$execCommand` which runs a single command
* - `$execCommands` which runs multiple commands
* Note: `$` is used to indicate that this function is not a pure function - it runs a command in a shell
*
* @public exported from `@promptbook/node`
*/
function $execCommand(options) {
if (!$isRunningInNode()) {
throw new EnvironmentMismatchError('Function `$execCommand` can run only in Node environment.js');
}
return new Promise((resolve, reject) => {
// eslint-disable-next-line prefer-const
const { command, humanReadableCommand, args, cwd, crashOnError, timeout, isVerbose = DEFAULT_IS_VERBOSE, env, } = $execCommandNormalizeOptions(options);
if (timeout !== Infinity) {
// TODO: In waitasecond forTime(Infinity) should be equivalent to forEver()
forTime(timeout).then(() => {
if (crashOnError) {
reject(new Error(`Command "${humanReadableCommand}" exceeded time limit of ${timeout}ms`));
}
else {
console.warn(`Command "${humanReadableCommand}" exceeded time limit of ${timeout}ms but continues running`);
// <- TODO: [๐ฎ] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
resolve('Command exceeded time limit');
}
});
}
if (isVerbose) {
console.info(colors.yellow(cwd) + ' ' + colors.green(command) + ' ' + colors.blue(args.join(' ')));
}
try {
const commandProcess = spawn(command, args, {
cwd,
shell: true,
env: env ? { ...process.env, ...env } : process.env,
});
if (isVerbose) {
commandProcess.on('message', (message) => {
console.info({ message });
});
}
const output = [];
commandProcess.stdout.on('data', (stdout) => {
output.push(stdout.toString());
if (isVerbose) {
console.info(stdout.toString());
}
});
commandProcess.stderr.on('data', (stderr) => {
output.push(stderr.toString());
if (isVerbose && stderr.toString().trim()) {
console.warn(stderr.toString());
// <- TODO: [๐ฎ] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
}
});
const finishWithCode = (code) => {
if (code !== 0) {
if (crashOnError) {
reject(new Error(output.join('\n').trim() ||
`Command "${humanReadableCommand}" exited with code ${code}`));
}
else {
if (isVerbose) {
console.warn(`Command "${humanReadableCommand}" exited with code ${code}`);
// <- TODO: [๐ฎ] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
}
resolve(spaceTrim$1(output.join('\n')));
}
}
else {
resolve(spaceTrim$1(output.join('\n')));
}
};
commandProcess.on('close', finishWithCode);
commandProcess.on('exit', finishWithCode);
commandProcess.on('disconnect', () => {
// Note: Unexpected disconnection should always result in rejection
reject(new Error(`Command "${humanReadableCommand}" disconnected`));
});
commandProcess.on('error', (error) => {
if (crashOnError) {
reject(new Error(`Command "${humanReadableCommand}" failed: \n${error.message}`));
}
else {
if (isVerbose) {
console.warn(error);
// <- TODO: [๐ฎ] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
}
resolve(spaceTrim$1(output.join('\n')));
}
});
}
catch (error) {
// Note: Unexpected error in sync code should always result in rejection
reject(error);
}
});
}
// Note: [๐ข] Code for Node command-execution helper [$execCommand](src/utils/execCommand/$execCommand.ts) should never be published into packages that could be imported into browser environment
/**
* [๐ฑโ๐]
* 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.
*
* TODO: [๐ฑโ๐] Change to `@promptbook/agent-server`
*
* @see https://github.com/webgptorg/promptbook#remote-server
*
* @public exported from `@promptbook/remote-server`
*/
async function startAgentServer(options) {
const { port = 4440 } = options;
// TODO: [๐ฑโ๐] [๐]
const agentsServerRoot = join(__dirname, '../apps/agents-server');
console.trace(`!!! Starting agents server on port ${port}...`);
console.log(`!!! cwd`, process.cwd());
console.log(`!!! __dirname`, __dirname);
console.log(`!!! agentsServerRoot`, agentsServerRoot);
await $execCommand({
cwd: agentsServerRoot,
command: `next dev --port ${port} `,
isVerbose: true,
});
}
// Note: [๐ข] Code for Node server bootstrap [startAgentServer](src/remote-server/startAgentServer.ts) should never be published into packages that could be imported into browser environment
/**
* Just says that the variable is not used but should be kept
* No side effects.
*
* Note: It can be useful for:
*
* 1) Suppressing eager optimization of unused imports
* 2) Suppressing eslint errors of unused variables in the tests
* 3) Keeping the type of the variable for type testing
*
* @param value any values
* @returns void
*
* @private within the repository
*/
function keepUnused(...valuesToKeep) {
}
/**
* Creates the base express application with shared middleware.
*
* @private internal utility of `startRemoteServer`
*/
function createRemoteServerExpressApp() {
const app = express();
app.use(express.json());
app.use(addPoweredByHeader);
return app;
}
/**
* Adds Promptbook branding header to each HTTP response.
*/
function addPoweredByHeader(request, response, next) {
response.setHeader('X-Powered-By', 'Promptbook engine');
next();
}
/**
* Creates the public RemoteServer handle with lazily exposed internals.
*
* @private internal utility of `startRemoteServer`
*/
function createRemoteServerHandle(app, httpServer, server) {
let isDestroyed = false;
return {
get httpServer() {
return httpServer;
},
get expressApp() {
return app;
},
get socketIoServer() {
return server;
},
get isDestroyed() {
return isDestroyed;
},
destroy() {
if (isDestroyed) {
return;
}
isDestroyed = true;
httpServer.close();
server.close();
},
};
}
/**
* Creates the Socket.io server with the existing transport and CORS settings.
*
* @private internal utility of `startRemoteServer`
*/
function createSocketServer(httpServer) {
return new Server(httpServer, {
path: '/socket.io',
transports: ['polling', 'websocket' /*, <- TODO: [๐ฌ] Allow to pass `transports`, add 'webtransport' */],
cors: {
origin: '*',
methods: ['GET', 'POST'],
// <- TODO: [๐ก] Allow to pass
},
});
}
/**
* Make error report URL for the given error
*
* @private private within the repository
*/
function getErrorReportUrl(error) {
const report = {
title: `๐ Error report from ${NAME}`,
body: spaceTrim$1((block) => `
\`${error.name || 'Error'}\` has occurred in the [${NAME}], please look into it @${ADMIN_GITHUB_NAME}.
\`\`\`
${block(error.message || '(no error message)')}
\`\`\`
## More info:
- **Promptbook engine version:** ${PROMPTBOOK_ENGINE_VERSION}
- **Book language version:** ${BOOK_LANGUAGE_VERSION}
- **Time:** ${new Date().toISOString()}
<details>
<summary>Stack trace:</summary>
## Stack trace:
\`\`\`stacktrace
${block(error.stack || '(empty)')}
\`\`\`
</details>
`),
};
const reportUrl = new URL(`https://github.com/webgptorg/promptbook/issues/new`);
reportUrl.searchParams.set('labels', 'bug');
reportUrl.searchParams.set('assignees', ADMIN_GITHUB_NAME);
report