@promptbook/openai
Version:
Promptbook: Run AI apps in plain human language across multiple models and platforms
3,161 lines โข 132 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('colors'), require('spacetrim'), require('crypto'), require('bottleneck'), require('openai'), require('socket.io-client')) :
typeof define === 'function' && define.amd ? define(['exports', 'colors', 'spacetrim', 'crypto', 'bottleneck', 'openai', 'socket.io-client'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["promptbook-openai"] = {}, global.colors, global.spaceTrim, global.crypto, global.Bottleneck, global.OpenAI, global.socket_ioClient));
})(this, (function (exports, colors, spaceTrim, crypto, Bottleneck, OpenAI, socket_ioClient) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var colors__default = /*#__PURE__*/_interopDefaultLegacy(colors);
var spaceTrim__default = /*#__PURE__*/_interopDefaultLegacy(spaceTrim);
var Bottleneck__default = /*#__PURE__*/_interopDefaultLegacy(Bottleneck);
var OpenAI__default = /*#__PURE__*/_interopDefaultLegacy(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 = '1.0.0';
/**
* The version of the Promptbook engine
*
* @generated
* @see https://github.com/webgptorg/promptbook
*/
const PROMPTBOOK_ENGINE_VERSION = '0.100.0-10';
/**
* 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`
*/
const $isRunningInBrowser = new Function(`
try {
return this === window;
} 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`
*/
const $isRunningInWebWorker = new Function(`
try {
if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) {
return true;
} else {
return false;
}
} catch (e) {
return false;
}
`);
/**
* TODO: [๐บ]
*/
/**
* This error type indicates that some part of the code is not implemented yet
*
* @public exported from `@promptbook/core`
*/
class NotYetImplementedError extends Error {
constructor(message) {
super(spaceTrim.spaceTrim((block) => `
${block(message)}
Note: This feature is not implemented yet but it will be soon.
If you want speed up the implementation or just read more, look here:
https://github.com/webgptorg/promptbook
Or contact us on pavol@ptbk.io
`));
this.name = 'NotYetImplementedError';
Object.setPrototypeOf(this, NotYetImplementedError.prototype);
}
}
/**
* Generates random token
*
* Note: This function is cryptographically secure (it uses crypto.randomBytes internally)
*
* @private internal helper function
* @returns secure random token
*/
function $randomToken(randomness) {
return crypto.randomBytes(randomness).toString('hex');
}
/**
* TODO: Maybe use nanoid instead https://github.com/ai/nanoid
*/
/**
* This error indicates errors during the execution of the pipeline
*
* @public exported from `@promptbook/core`
*/
class PipelineExecutionError extends Error {
constructor(message) {
// Added id parameter
super(message);
this.name = 'PipelineExecutionError';
// TODO: [๐] DRY - Maybe $randomId
this.id = `error-${$randomToken(8 /* <- TODO: To global config + Use Base58 to avoid similar char conflicts */)}`;
Object.setPrototypeOf(this, PipelineExecutionError.prototype);
}
}
/**
* TODO: [๐ง ][๐] Add id to all errors
*/
/**
* 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
*/
/**
* 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`
*/
$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
*/
/**
* 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();
}
/**
* 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: [๐ง ] 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;
/**
* 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;
}
/**
* 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__default["default"]((block) => `
\`${error.name || 'Error'}\` has occurred in the [${NAME}], please look into it @${ADMIN_GITHUB_NAME}.
\`\`\`
${block(error.message || '(no error message)')}
\`\`\`
## More info:
- **Promptbook engine version:** ${PROMPTBOOK_ENGINE_VERSION}
- **Book language version:** ${BOOK_LANGUAGE_VERSION}
- **Time:** ${new Date().toISOString()}
<details>
<summary>Stack trace:</summary>
## Stack trace:
\`\`\`stacktrace
${block(error.stack || '(empty)')}
\`\`\`
</details>
`),
};
const reportUrl = new URL(`https://github.com/webgptorg/promptbook/issues/new`);
reportUrl.searchParams.set('labels', 'bug');
reportUrl.searchParams.set('assignees', ADMIN_GITHUB_NAME);
reportUrl.searchParams.set('title', report.title);
reportUrl.searchParams.set('body', report.body);
return reportUrl;
}
/**
* This error type indicates that the error should not happen and its last check before crashing with some other error
*
* @public exported from `@promptbook/core`
*/
class UnexpectedError extends Error {
constructor(message) {
super(spaceTrim.spaceTrim((block) => `
${block(message)}
Note: This error should not happen.
It's probably a bug in the pipeline collection
Please report issue:
${block(getErrorReportUrl(new Error(message)).href)}
Or contact us on ${ADMIN_EMAIL}
`));
this.name = 'UnexpectedError';
Object.setPrototypeOf(this, UnexpectedError.prototype);
}
}
/**
* This error type indicates that somewhere in the code non-Error object was thrown and it was wrapped into the `WrappedError`
*
* @public exported from `@promptbook/core`
*/
class WrappedError extends Error {
constructor(whatWasThrown) {
const tag = `[๐คฎ]`;
console.error(tag, whatWasThrown);
super(spaceTrim.spaceTrim(`
Non-Error object was thrown
Note: Look for ${tag} in the console for more details
Please report issue on ${ADMIN_EMAIL}
`));
this.name = 'WrappedError';
Object.setPrototypeOf(this, WrappedError.prototype);
}
}
/**
* 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__default["default"]((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__default["default"]((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__default["default"]((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__default["default"]((block) => `
\`${name}\` is unknown type
Additional message for \`${name}\`:
${block(message || '(nothing)')}
`));
}
}
/**
* TODO: Can be return type more type-safe? like `asserts options.value is JsonValue`
* TODO: [๐ง ][main] !!3 In-memory cache of same values to prevent multiple checks
* Note: [๐ ] This is how `checkSerializableAsJson` + `isSerializableAsJson` together can just retun true/false or rich error message
*/
/**
* Creates a deep clone of the given object
*
* Note: This method only works for objects that are fully serializable to JSON and do not contain functions, Dates, or special types.
*
* @param objectValue The object to clone.
* @returns A deep, writable clone of the input object.
* @public exported from `@promptbook/utils`
*/
function deepClone(objectValue) {
return JSON.parse(JSON.stringify(objectValue));
/*
TODO: [๐ง ] Is there a better implementation?
> const propertyNames = Object.getOwnPropertyNames(objectValue);
> for (const propertyName of propertyNames) {
> const value = (objectValue as really_any)[propertyName];
> if (value && typeof value === 'object') {
> deepClone(value);
> }
> }
> return Object.assign({}, objectValue);
*/
}
/**
* TODO: [๐ง ] Is there a way how to meaningfully test this utility
*/
/**
* Utility to export a JSON object from a function
*
* 1) Checks if the value is serializable as JSON
* 2) Makes a deep clone of the object
* 2) Orders the object properties
* 2) Deeply freezes the cloned object
*
* Note: This function does not mutates the given object
*
* @returns The same type of object as the input but read-only and re-ordered
* @public exported from `@promptbook/utils`
*/
function exportJson(options) {
const { name, value, order, message } = options;
checkSerializableAsJson({ name, value, message });
const orderedValue =
// TODO: Fix error "Type instantiation is excessively deep and possibly infinite."
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
order === undefined
? deepClone(value)
: orderJson({
value: value,
// <- Note: checkSerializableAsJson asserts that the value is serializable as JSON
order: order,
});
$deepFreeze(orderedValue);
return orderedValue;
}
/**
* TODO: [๐ง ] Is there a way how to meaningfully test this utility
*/
/**
* 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
*/
/**
* 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);
}
}
/**
* 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;
}
/**
* 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,...
*/
/**
* 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 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) {
text = text.replace('\r\n', '\n');
text = text.replace('\r', '\n');
const lines = text.split('\n');
return lines.reduce((count, line) => count + Math.ceil(line.length / CHARACTERS_PER_STANDARD_LINE), 0);
}
/**
* TODO: [๐ฅด] Implement counting in formats - like JSON, CSV, XML,...
*/
/**
* 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,...
*/
/**
* 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,...
*/
/**
* 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,...
*/
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.
*
* @param input The string containing diacritics to be normalized.
* @returns The string with diacritics removed or normalized.
* @public exported from `@promptbook/utils`
*/
function removeDiacritics(input) {
/*eslint no-control-regex: "off"*/
return input.replace(/[^\u0000-\u007E]/g, (a) => {
return DIACRITIC_VARIANTS_LETTERS[a] || a;
});
}
/**
* TODO: [ะ] Variant for cyrillic (and in general non-latin) letters
*/
/**
* 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,...
*/
/**
* 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: Done at 2025-05-06
*
* @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: [
/*/
{
modelTitle: 'dall-e-3',
modelName: 'dall-e-3',
},
/**/
/*/
{
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`),
},
},
/**/
/*/
{
modelTitle: 'dall-e-2',
modelName: 'dall-e-2',
},
/**/
/**/
{
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`
*/
/**
* Execution Tools for calling OpenAI API or other OpenAI compatible provider
*
* @public exported from `@promptbook/openai`
*/
class OpenAiCompatibleExecutionTools {
/**
* 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__default["default"]({
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;
this.client = new OpenAI__default["default"](openAiOptions);
}
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.
*/
async callChatModel(prompt) {
var _a;
if (this.options.isVerbose) {
console.info(`๐ฌ ${this.title} 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');
}
const modelName = modelRequirements.modelName || this.getDefaultChatModel().modelName;
const modelSettings = {
model: modelName,
max_tokens: modelRequirements.maxTokens,
// <- TODO: [๐พ] Make some global max cap for maxTokens
temperature: modelRequirements.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 });
const rawRequest = {
...modelSettings,
messages: [
...(modelRequirements.systemMessage === undefined
? []
: [
{
role: 'system',
content: modelRequirements.systemMessage,
},
]),
{
role: 'user',
content: rawPromptContent,
},
],
user: (_a = this.options.userId) === null || _a === void 0 ? void 0 : _a.toString(),
};
const start = $getCurrentDate();
if (this.options.isVerbose) {
console.info(colors__default["default"].bgWhite('rawRequest'), JSON.stringify(rawRequest, null, 4));
}
const rawResponse = await this.limiter
.schedule(() => client.chat.completions.create(rawRequest))
.catch((error) => {
assertsError(error);
if (this.options.isVerbose) {
console.info(colors__default["default"].bgRed('error'), error);
}
throw error;
});
if (this.options.isVerbose) {
console.info(colors__default["default"].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) {
// TODO: This should be maybe only warning
throw new PipelineExecutionError(`More than one choise from ${this.title}`);
}
const resultContent = rawResponse.choices[0].message.content;
const usage = this.computeUsage(content || '', resultContent || '', rawResponse);
if (resultContent === null) {
throw new PipelineExecutionError(`No response message from ${this.title}`);
}
return exportJson({
name: 'promptResult',
message: `Result of \`OpenAiCompatibleExecutionTools.callChatModel\``,
order: [],
value: {
content: resultContent,
modelName: rawResponse.model || modelName,
timing: {
start,
complete,
},
usage,
rawPromptContent,
rawRequest,
rawResponse,
// <- [๐ฏ]
},
});
}
/**
* Calls OpenAI API to use a complete model.
*/
async callCompletionModel(prompt) {
var _a;
if (this.options.isVerbose) {
console.info(`๐ ${this.title} callCompletionModel call`, { prompt });
}
const { content, parameters, modelRequirements } = prompt;
const client = await this.getClient();
// TODO: [โ] Use here more modelRequirements
if (modelRequirements.modelVariant !== 'COMPLETION') {
throw new PipelineExecutionError('Use callCompletionModel only for COMPLETION variant');
}
const modelName = modelRequirements.modelName || this.getDefaultCompletionModel().modelName;
const modelSettings = {
model: modelName,
max_tokens: modelRequirements.maxTokens || 2000,
// <- TODO: [๐พ] Make some global max cap for maxTokens
temperature: modelRequirements.temperature,
// <- TODO: [๐] Use `seed` here AND/OR use is `isDeterministic` for entire execution tools
// <- Note: [๐ง]
};
const rawPromptContent = templateParameters(content, { ...parameters, modelName });
const rawRequest = {
...modelSettings,
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__default["default"].bgWhite('rawRequest'), JSON.stringify(rawRequest, null, 4));
}
const rawResponse = await this.limiter
.schedule(() => client.completions.create(rawRequest))
.catch((error) => {
assertsError(error);
if (this.options.isVerbose) {
console.info(colors__default["default"].bgRed('error'), error);
}
throw error;
});
if (this.options.isVerbose) {
console.info(colors__default["default"].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) {
// TODO: This should be maybe only warning
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,
// <- [๐ฏ]
},
});
}
/**
* Calls OpenAI compatible API to use a embedding model
*/
async callEmbeddingModel(prompt) {
if (this.options.isVerbose) {
console.info(`๐ ${this.title} embedding call`, { prompt });
}
const { content, parameters, modelRequirements } = prompt;
const client = await this.getClient();
// TODO: [โ] Use here more modelRequirements
if (modelRequirements.modelVariant !== 'EMBEDDING') {
throw new PipelineExecutionError('Use embed only for EMBEDDING variant');
}
const modelName = modelRequirements.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__default["default"].bgWhite('rawRequest'), JSON.stringify(rawRequest, null, 4));
}
const rawResponse = await this.limiter
.schedule(() => client.embeddings.create(rawRequest))
.catch((error) => {
assertsError(error);
if (this.options.isVerbose) {
console.info(colors__default["default"].bgRed('error'), error);
}
throw error;
});
if (this.options.isVerbose) {
console.info(colors__default["default"].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 || '', '',
// <- Note: Embedding does not have result 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,
// <- [๐ฏ]
},
});
}
// <- 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__default["default"]((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;
}
}
/**
* 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
*/
/**
* 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';
}
/*
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-4-turbo');
}
/**
* 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');
}
}
/**
* Execution Tools for calling OpenAI API Assistants
*
* This is useful for calling OpenAI API with a single assistant, for more wide usage use `OpenAiExecutionTools`.
*
* @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) {
if (options.isProxied) {
throw new NotYetImplementedError(`Proxy mode is not yet implemented for OpenAI assistants`);
}
super(options);
this.assistantId = options.assistantId;
// 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) {
var _a, _b, _c;
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,
max_tokens: modelRequirements.maxTokens,
// <- TODO: [๐พ] Make some global max cap for maxTokens
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
});
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: [
// TODO: [๐ฏ] Allow threads to be passed
{ role: 'user', content: rawPromptContent },
],
},
// <- TODO: Add user identification here> user: this.options.user,
};
const start = $getCurrentDate();
let complete;
if (this.options.isVerbose) {
console.info(colors__default["default"].bgWhite('rawRequest'), 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('messageDelta', (messageDelta) => {
var _a;
if (this.options.isVerbose &&
messageDelta &&
messageDelta.content &&
messageDelta.content[0] &&
messageDelta.content[0].type === 'text') {
console.info('messageDelta', (_a = messageDelta.content[0].text) === null || _a === void 0 ? void 0 : _a.value);
}
// <- TODO: [๐] Make streaming and running tasks working
});
stream.on('messageCreated', (message) => {
if (this.options.isVerbose) {
console.info('messageCreated', message);
}
});
stream.on('messageDone', (message) => {
if (this.options.isVerbose) {
console.info('messageDone', message);
}
});
const rawResponse = await stream.finalMessages();
if (this.options.isVerbose) {
console.info(colors__default["default"].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 (((_a = rawResponse[0].content[0]) === null || _a === void 0 ? void 0 : _a.type) !== 'text') {
throw new PipelineExecutionError(`There is NOT 'text' BUT ${(_b = rawResponse[0].content[0]) === null || _b === void 0 ? void 0 : _b.type} finalMessages content type from OpenAI`);
}
const resultContent = (_c = rawResponse[0].content[0]) === null || _c === void 0 ? void 0 : _c.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.callChatModel\``,
order: [],
value: {
content: resultContent,
modelName: 'assistant',
// <- TODO: [๐ฅ] Detect used model in assistant
// ?> model: rawResponse.model || modelName,
timing: {
start,
complete,
},
usage,
rawPromptContent,
rawRequest,
rawResponse,
// <- [๐ฏ]
},
});
}
}
/**
* 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"
*/
/**
* This error indicates problems parsing the format value
*
* For example, when the format value is not a valid JSON or CSV
* This is not thrown directly but in extended classes
*
* @public exported from `@promptbook/core`
*/
class AbstractFormatError extends Error {
// Note: To allow instanceof do not put here error `name`
// public readonly name = 'AbstractFormatError';
constructor(message) {
super(message);
Object.setPrototypeOf(this, AbstractFormatError.prototype);
}
}
/**
* This error indicates problem with parsing of CSV
*
* @public exported from `@promptbook/core`
*/
class CsvFormatError extends AbstractFormatError {
constructor(message) {
super(message);
this.name = 'CsvFormatError';
Object.setPrototypeOf(this, CsvFormatError.prototype);
}
}
/**
* AuthenticationError is thrown from login function which is dependency of remote server
*
* @public exported from `@promptbook/core`
*/
class AuthenticationError extends Error {
constructor(message) {
super(message);
this.name = 'AuthenticationError';
Object.setPrototypeOf(this, AuthenticationError.prototype);
}
}
/**
* This error indicates that the pipeline collection cannot be properly loaded
*
* @public exported from `@promptbook/core`
*/
class CollectionError extends Error {
constructor(message) {
super(message);
this.name = 'CollectionError';
Object.setPrototypeOf(this, CollectionError.prototype);
}
}
/**
* This error type indicates that you try to use a feature that is not available in the current environment
*
* @public exported from `@promptbook/core`
*/
class EnvironmentMismatchError extends Error {
constructor(message) {
super(message);
this.name = 'EnvironmentMismatchError';
Object.setPrototypeOf(this, EnvironmentMismatchError.prototype);
}
}
/**
* This error occurs when some expectation is not met in the execution of the pipeline
*
* @public exported from `@promptbook/core`
* Note: Do not throw this error, its reserved for `checkExpectations` and `createPipelineExecutor` and public ONLY to be serializable through remote server
* Note: Always thrown in `checkExpectations` and catched in `createPipelineExecutor` and rethrown as `PipelineExecutionError`
* Note: This is a kindof subtype of PipelineExecutionError
*/
class ExpectError extends Error {
constructor(message) {
super(message);
this.name = 'ExpectError';
Object.setPrototypeOf(this, ExpectError.prototype);
}
}
/**
* This error indicates that the promptbook can not retrieve knowledge from external sources
*
* @public exported from `@promptbook/core`
*/
class KnowledgeScrapeError extends Error {
constructor(message) {
super(message);
this.name = 'KnowledgeScrapeError';
Object.setPrototypeOf(this, KnowledgeScrapeError.prototype);
}
}
/**
* This error type indicates that some tools are missing for pipeline execution or preparation
*
* @public exported from `@promptbook/core`
*/
class MissingToolsError extends Error {
constructor(message) {
super(spaceTrim.spaceTrim((block) => `
${block(message)}
Note: You have probably forgot to provide some tools for pipeline execution or preparation
`));
this.name = 'MissingToolsError';
Object.setPrototypeOf(this, MissingToolsError.prototype);
}
}
/**
* This error indicates that promptbook not found in the collection
*
* @public exported from `@promptbook/core`
*/
class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = 'NotFoundError';
Object.setPrototypeOf(this, NotFoundError.prototype);
}
}
/**
* This error indicates that the promptbook in a markdown format cannot be parsed into a valid promptbook object
*
* @public exported from `@promptbook/core`
*/
class ParseError extends Error {
constructor(message) {
super(message);
this.name = 'ParseError';
Object.setPrototypeOf(this, ParseError.prototype);
}
}
/**
* TODO: Maybe split `ParseError` and `ApplyError`
*/
/**
* This error indicates that the promptbook object has valid syntax (=can be parsed) but contains logical errors (like circular dependencies)
*
* @public exported from `@promptbook/core`
*/
class PipelineLogicError extends Error {
constructor(message) {
super(message);
this.name = 'PipelineLogicError';
Object.setPrototypeOf(this, PipelineLogicError.prototype);
}
}
/**
* This error indicates errors in referencing promptbooks between each other
*
* @public exported from `@promptbook/core`
*/
class PipelineUrlError extends Error {
constructor(message) {
super(message);
this.name = 'PipelineUrlError';
Object.setPrototypeOf(this, PipelineUrlError.prototype);
}
}
/**
* Error thrown when a fetch request fails
*
* @public exported from `@promptbook/core`
*/
class PromptbookFetchError extends Error {
constructor(message) {
super(message);
this.name = 'PromptbookFetchError';
Object.setPrototypeOf(this, PromptbookFetchError.prototype);
}
}
/**
* Index of all custom errors
*
* @public exported from `@promptbook/core`
*/
const PROMPTBOOK_ERRORS = {
AbstractFormatError,
CsvFormatError,
CollectionError,
EnvironmentMismatchError,
ExpectError,
KnowledgeScrapeError,
LimitReachedError,
MissingToolsError,
NotFoundError,
NotYetImplementedError,
ParseError,
PipelineExecutionError,
PipelineLogicError,
PipelineUrlError,
AuthenticationError,
PromptbookFetchError,
UnexpectedError,
WrappedError,
// TODO: [๐ช]> VersionMismatchError,
};
/**
* Index of all javascript errors
*
* @private for internal usage
*/
const COMMON_JAVASCRIPT_ERRORS = {
Error,
EvalError,
RangeError,
ReferenceError,
SyntaxError,
TypeError,
URIError,
AggregateError,
/*
Note: Not widely supported
> InternalError,
> ModuleError,
> HeapError,
> WebAssemblyCompileError,
> WebAssemblyRuntimeError,
*/
};
/**
* Index of all errors
*
* @private for internal usage
*/
const ALL_ERRORS = {
...PROMPTBOOK_ERRORS,
...COMMON_JAVASCRIPT_ERRORS,
};
/**
* Note: [๐] Ignore a discrepancy between file name and entity name
*/
/**
* Deserializes the error object
*
* @public exported from `@promptbook/utils`
*/
function deserializeError(error) {
const { name, stack, id } = error; // Added id
let { message } = error;
let ErrorClass = ALL_ERRORS[error.name];
if (ErrorClass === undefined) {
ErrorClass = Error;
message = `${name}: ${message}`;
}
if (stack !== undefined && stack !== '') {
message = spaceTrim__default["default"]((block) => `
${block(message)}
Original stack trace:
${block(stack || '')}
`);
}
const deserializedError = new ErrorClass(message);
deserializedError.id = id; // Assign id to the error object
return deserializedError;
}
/**
* Tests if given string is valid URL.
*
* Note: Dataurl are considered perfectly valid.
* Note: There are two similar functions:
* - `isValidUrl` which tests any URL
* - `isValidPipelineUrl` *(this one)* which tests just promptbook URL
*
* @public exported from `@promptbook/utils`
*/
function isValidUrl(url) {
if (typeof url !== 'string') {
return false;
}
try {
if (url.startsWith('blob:')) {
url = url.replace(/^blob:/, '');
}
const urlObject = new URL(url /* because fail is handled */);
if (!['http:', 'https:', 'data:'].includes(urlObject.protocol)) {
return false;
}
return true;
}
catch (error) {
return false;
}
}
/**
* Creates a connection to the remote proxy server.
*
* Note: This function creates a connection to the remote server and returns a socket but responsibility of closing the connection is on the caller
*
* @private internal utility function
*/
async function createRemoteClient(options) {
const { remoteServerUrl } = options;
if (!isValidUrl(remoteServerUrl)) {
throw new Error(`Invalid \`remoteServerUrl\`: "${remoteServerUrl}"`);
}
const remoteServerUrlParsed = new URL(remoteServerUrl);
if (remoteServerUrlParsed.pathname !== '/' && remoteServerUrlParsed.pathname !== '') {
remoteServerUrlParsed.pathname = '/';
throw new Error(spaceTrim__default["default"]((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 = socket_ioClient.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);
});
}
/**
* Remote server is a proxy server that uses its execution tools internally and exposes the executor interface externally.
*
* You can simply use `RemoteExecutionTools` on client-side javascript and connect to your remote server.
* This is useful to make all logic on browser side but not expose your API keys or no need to use customer's GPU.
*
* @see https://github.com/webgptorg/promptbook#remote-server
* @public exported from `@promptbook/remote-client`
*/
class RemoteLlmExecutionTools {
/* <- TODO: [๐] `, Destroyable` */
constructor(options) {
this.options = options;
}
get title() {
// TODO: [๐ง ] Maybe fetch title+description from the remote server (as well as if model methods are defined)
return 'Promptbook remote server';
}
get description() {
return `Models from Promptbook remote server ${this.options.remoteServerUrl}`;
}
/**
* Check the configuration of all execution tools
*/
async checkConfiguration() {
const socket = await createRemoteClient(this.options);
socket.disconnect();
// TODO: [main] !!3 Check version of the remote server and compatibility
// TODO: [๐] Send checkConfiguration
}
/**
* List all available models that can be used
*/
async listModels() {
// TODO: [๐] Listing models (and checking configuration) probably should go through REST API not Socket.io
const socket = await createRemoteClient(this.options);
socket.emit('listModels-request', {
identification: this.options.identification,
} /* <- Note: [๐ค] */);
const promptResult = await new Promise((resolve, reject) => {
socket.on('listModels-response', (response) => {
resolve(response.models);
socket.disconnect();
});
socket.on('error', (error) => {
reject(deserializeError(error));
socket.disconnect();
});
});
socket.disconnect();
return promptResult;
}
/**
* Calls remote proxy server to use a chat model
*/
callChatModel(prompt) {
if (this.options.isVerbose) {
console.info(`๐ Remote callChatModel call`);
}
return /* not await */ this.callCommonModel(prompt);
}
/**
* Calls remote proxy server to use a completion model
*/
callCompletionModel(prompt) {
if (this.options.isVerbose) {
console.info(`๐ฌ Remote callCompletionModel call`);
}
return /* not await */ this.callCommonModel(prompt);
}
/**
* Calls remote proxy server to use a embedding model
*/
callEmbeddingModel(prompt) {
if (this.options.isVerbose) {
console.info(`๐ฌ Remote callEmbeddingModel call`);
}
return /* not await */ this.callCommonModel(prompt);
}
// <- Note: [๐ค] callXxxModel
/**
* Calls remote proxy server to use both completion or chat model
*/
async callCommonModel(prompt) {
const socket = await createRemoteClient(this.options);
socket.emit('prompt-request', {
identification: this.options.identification,
prompt,
} /* <- Note: [๐ค] */);
const promptResult = await new Promise((resolve, reject) => {
socket.on('prompt-response', (response) => {
resolve(response.promptResult);
socket.disconnect();
});
socket.on('error', (error) => {
reject(deserializeError(error));
socket.disconnect();
});
});
socket.disconnect();
return promptResult;
}
}
/**
* TODO: Maybe use `$exportJson`
* TODO: [๐ง ][๐] Maybe not `isAnonymous: boolean` BUT `mode: 'ANONYMOUS'|'COLLECTION'`
* TODO: [๐] Allow to list compatible models with each variant
* TODO: [๐ฏ] RemoteLlmExecutionTools should extend Destroyable and implement IDestroyable
* TODO: [๐ง ][๐ฐ] Allow to pass `title` for tracking purposes
* TODO: [๐ง ] Maybe remove `@promptbook/remote-client` and just use `@promptbook/core`
*/
/**
* 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`);
}
}
/**
* 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')();
}
/**
* Normalizes a text string to SCREAMING_CASE (all uppercase with underscores).
*
* @param text The text string to be converted to SCREAMING_CASE format.
* @returns The normalized text in SCREAMING_CASE format.
* @example 'HELLO_WORLD'
* @example 'I_LOVE_PROMPTBOOK'
* @public exported from `@promptbook/utils`
*/
function normalizeTo_SCREAMING_CASE(text) {
let charType;
let lastCharType = 'OTHER';
let normalizedName = '';
for (const char of text) {
let normalizedChar;
if (/^[a-z]$/.test(char)) {
charType = 'LOWERCASE';
normalizedChar = char.toUpperCase();
}
else if (/^[A-Z]$/.test(char)) {
charType = 'UPPERCASE';
normalizedChar = char;
}
else if (/^[0-9]$/.test(char)) {
charType = 'NUMBER';
normalizedChar = char;
}
else {
charType = 'OTHER';
normalizedChar = '_';
}
if (charType !== lastCharType &&
!(lastCharType === 'UPPERCASE' && charType === 'LOWERCASE') &&
!(lastCharType === 'NUMBER') &&
!(charType === 'NUMBER')) {
normalizedName += '_';
}
normalizedName += normalizedChar;
lastCharType = charType;
}
normalizedName = normalizedName.replace(/_+/g, '_');
normalizedName = normalizedName.replace(/_?\/_?/g, '/');
normalizedName = normalizedName.replace(/^_/, '');
normalizedName = normalizedName.replace(/_$/, '');
return normalizedName;
}
/**
* TODO: Tests
* > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: 'Moje tabule' })).toEqual('/VtG7sR9rRJqwNEdM2/Moje tabule');
* > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: 'ฤลกฤลลพลพรฝรกรญรบลฏ' })).toEqual('/VtG7sR9rRJqwNEdM2/escrzyaieuu');
* > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: ' ahoj ' })).toEqual('/VtG7sR9rRJqwNEdM2/ahoj');
* > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: ' ahoj_ahojAhoj ahoj ' })).toEqual('/VtG7sR9rRJqwNEdM2/ahoj-ahoj-ahoj-ahoj');
* TODO: [๐บ] Use some intermediate util splitWords
*/
/**
* Normalizes a text string to snake_case format.
*
* @param text The text string to be converted to snake_case format.
* @returns The normalized text in snake_case format.
* @example 'hello_world'
* @example 'i_love_promptbook'
* @public exported from `@promptbook/utils`
*/
function normalizeTo_snake_case(text) {
return normalizeTo_SCREAMING_CASE(text).toLowerCase();
}
/**
* Global registry for storing and managing registered entities of a given type.
*
* Note: `$` is used to indicate that this function is not a pure function - it accesses and adds variables in global scope.
*
* @private internal utility, exported are only singleton instances of this class
*/
class $Register {
constructor(registerName) {
this.registerName = registerName;
const storageName = `_promptbook_${normalizeTo_snake_case(registerName)}`;
const globalScope = $getGlobalScope();
if (globalScope[storageName] === undefined) {
globalScope[storageName] = [];
}
else if (!Array.isArray(globalScope[storageName])) {
throw new UnexpectedError(`Expected (global) ${storageName} to be an array, but got ${typeof globalScope[storageName]}`);
}
this.storage = globalScope[storageName];
}
list() {
// <- TODO: ReadonlyDeep<ReadonlyArray<TRegistered>>
return this.storage;
}
register(registered) {
const { packageName, className } = registered;
const existingRegistrationIndex = this.storage.findIndex((item) => item.packageName === packageName && item.className === className);
const existingRegistration = this.storage[existingRegistrationIndex];
if (!existingRegistration) {
this.storage.push(registered);
}
else {
this.storage[existingRegistrationIndex] = registered;
}
return {
registerName: this.registerName,
packageName,
className,
get isDestroyed() {
return false;
},
destroy() {
throw new NotYetImplementedError(`Registration to ${this.registerName} is permanent in this version of Promptbook`);
},
};
}
}
/**
* Register for LLM tools.
*
* 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
*/
exports.BOOK_LANGUAGE_VERSION = BOOK_LANGUAGE_VERSION;
exports.OPENAI_MODELS = OPENAI_MODELS;
exports.OpenAiAssistantExecutionTools = OpenAiAssistantExecutionTools;
exports.OpenAiCompatibleExecutionTools = OpenAiCompatibleExecutionTools;
exports.OpenAiExecutionTools = OpenAiExecutionTools;
exports.PROMPTBOOK_ENGINE_VERSION = PROMPTBOOK_ENGINE_VERSION;
exports._OpenAiAssistantRegistration = _OpenAiAssistantRegistration;
exports._OpenAiCompatibleRegistration = _OpenAiCompatibleRegistration;
exports._OpenAiRegistration = _OpenAiRegistration;
exports.createOpenAiAssistantExecutionTools = createOpenAiAssistantExecutionTools;
exports.createOpenAiCompatibleExecutionTools = createOpenAiCompatibleExecutionTools;
exports.createOpenAiExecutionTools = createOpenAiExecutionTools;
Object.defineProperty(exports, '__esModule', { value: true });
}));
//# sourceMappingURL=index.umd.js.map