@promptbook/cli 0.88.0 → 0.89.0-11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +7 -1
  2. package/esm/index.es.js +2152 -1545
  3. package/esm/index.es.js.map +1 -1
  4. package/esm/typings/src/_packages/core.index.d.ts +18 -6
  5. package/esm/typings/src/_packages/remote-client.index.d.ts +6 -8
  6. package/esm/typings/src/_packages/remote-server.index.d.ts +6 -6
  7. package/esm/typings/src/_packages/types.index.d.ts +18 -10
  8. package/esm/typings/src/cli/cli-commands/login.d.ts +15 -0
  9. package/esm/typings/src/cli/common/$addGlobalOptionsToCommand.d.ts +7 -0
  10. package/esm/typings/src/cli/common/$provideLlmToolsForCli.d.ts +15 -0
  11. package/esm/typings/src/config.d.ts +15 -8
  12. package/esm/typings/src/errors/0-index.d.ts +6 -0
  13. package/esm/typings/src/errors/AuthenticationError.d.ts +9 -0
  14. package/esm/typings/src/errors/PromptbookFetchError.d.ts +9 -0
  15. package/esm/typings/src/execution/PipelineExecutorResult.d.ts +2 -2
  16. package/esm/typings/src/execution/PromptResult.d.ts +2 -2
  17. package/esm/typings/src/execution/{PromptResultUsage.d.ts → Usage.d.ts} +5 -5
  18. package/esm/typings/src/execution/utils/addUsage.d.ts +2 -2
  19. package/esm/typings/src/execution/utils/computeUsageCounts.d.ts +3 -3
  20. package/esm/typings/src/execution/utils/usage-constants.d.ts +77 -60
  21. package/esm/typings/src/execution/utils/usageToHuman.d.ts +5 -5
  22. package/esm/typings/src/execution/utils/usageToWorktime.d.ts +5 -5
  23. package/esm/typings/src/llm-providers/_common/register/$provideEnvFilename.d.ts +12 -0
  24. package/esm/typings/src/llm-providers/_common/register/$provideLlmToolsConfigurationFromEnv.d.ts +2 -8
  25. package/esm/typings/src/llm-providers/_common/register/$provideLlmToolsForTestingAndScriptsAndPlayground.d.ts +2 -0
  26. package/esm/typings/src/llm-providers/_common/register/$provideLlmToolsForWizzardOrCli.d.ts +36 -1
  27. package/esm/typings/src/llm-providers/_common/register/$provideLlmToolsFromEnv.d.ts +1 -0
  28. package/esm/typings/src/llm-providers/_common/utils/count-total-usage/LlmExecutionToolsWithTotalUsage.d.ts +9 -2
  29. package/esm/typings/src/llm-providers/_common/utils/count-total-usage/{countTotalUsage.d.ts → countUsage.d.ts} +1 -1
  30. package/esm/typings/src/llm-providers/_common/utils/count-total-usage/limitTotalUsage.d.ts +2 -2
  31. package/esm/typings/src/llm-providers/anthropic-claude/AnthropicClaudeExecutionToolsOptions.d.ts +1 -1
  32. package/esm/typings/src/llm-providers/anthropic-claude/computeAnthropicClaudeUsage.d.ts +2 -2
  33. package/esm/typings/src/llm-providers/anthropic-claude/register-configuration.d.ts +1 -1
  34. package/esm/typings/src/llm-providers/openai/OpenAiExecutionTools.d.ts +0 -9
  35. package/esm/typings/src/llm-providers/openai/computeOpenAiUsage.d.ts +2 -2
  36. package/esm/typings/src/pipeline/PipelineJson/PreparationJson.d.ts +2 -2
  37. package/esm/typings/src/playground/playground.d.ts +5 -0
  38. package/esm/typings/src/remote-server/RemoteServer.d.ts +23 -0
  39. package/esm/typings/src/remote-server/socket-types/_subtypes/{PromptbookServer_Identification.d.ts → Identification.d.ts} +5 -4
  40. package/esm/typings/src/remote-server/socket-types/listModels/PromptbookServer_ListModels_Request.d.ts +2 -2
  41. package/esm/typings/src/remote-server/socket-types/prepare/PromptbookServer_PreparePipeline_Request.d.ts +2 -2
  42. package/esm/typings/src/remote-server/socket-types/prompt/PromptbookServer_Prompt_Request.d.ts +2 -2
  43. package/esm/typings/src/remote-server/startRemoteServer.d.ts +2 -2
  44. package/esm/typings/src/remote-server/types/RemoteClientOptions.d.ts +4 -12
  45. package/esm/typings/src/remote-server/types/RemoteServerOptions.d.ts +88 -6
  46. package/esm/typings/src/scrapers/_common/utils/{scraperFetch.d.ts → promptbookFetch.d.ts} +2 -2
  47. package/esm/typings/src/storage/env-storage/$EnvStorage.d.ts +37 -0
  48. package/esm/typings/src/types/typeAliases.d.ts +8 -2
  49. package/esm/typings/src/utils/organization/TODO_narrow.d.ts +6 -0
  50. package/package.json +3 -1
  51. package/umd/index.umd.js +2169 -1562
  52. package/umd/index.umd.js.map +1 -1
package/umd/index.umd.js CHANGED
@@ -1,8 +1,8 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('colors'), require('commander'), require('spacetrim'), require('waitasecond'), require('path'), require('fs/promises'), require('crypto-js/enc-hex'), require('crypto-js/sha256'), require('crypto'), require('dotenv'), require('child_process'), require('jszip'), require('prettier'), require('prettier/parser-html'), require('rxjs'), require('papaparse'), require('crypto-js'), require('mime-types'), require('glob-promise'), require('prompts'), require('moment'), require('express'), require('http'), require('socket.io'), require('socket.io-client'), require('@anthropic-ai/sdk'), require('@azure/openai'), require('openai'), require('@mozilla/readability'), require('jsdom'), require('showdown')) :
3
- typeof define === 'function' && define.amd ? define(['exports', 'colors', 'commander', 'spacetrim', 'waitasecond', 'path', 'fs/promises', 'crypto-js/enc-hex', 'crypto-js/sha256', 'crypto', 'dotenv', 'child_process', 'jszip', 'prettier', 'prettier/parser-html', 'rxjs', 'papaparse', 'crypto-js', 'mime-types', 'glob-promise', 'prompts', 'moment', 'express', 'http', 'socket.io', 'socket.io-client', '@anthropic-ai/sdk', '@azure/openai', 'openai', '@mozilla/readability', 'jsdom', 'showdown'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["promptbook-cli"] = {}, global.colors, global.commander, global.spaceTrim, global.waitasecond, global.path, global.promises, global.hexEncoder, global.sha256, global.crypto, global.dotenv, global.child_process, global.JSZip, global.prettier, global.parserHtml, global.rxjs, global.papaparse, global.cryptoJs, global.mimeTypes, global.glob, global.prompts, global.moment, global.express, global.http, global.socket_io, global.socket_ioClient, global.Anthropic, global.openai, global.OpenAI, global.readability, global.jsdom, global.showdown));
5
- })(this, (function (exports, colors, commander, spaceTrim, waitasecond, path, promises, hexEncoder, sha256, crypto, dotenv, child_process, JSZip, prettier, parserHtml, rxjs, papaparse, cryptoJs, mimeTypes, glob, prompts, moment, express, http, socket_io, socket_ioClient, Anthropic, openai, OpenAI, readability, jsdom, showdown) { 'use strict';
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('colors'), require('commander'), require('spacetrim'), require('waitasecond'), require('prompts'), require('path'), require('fs/promises'), require('dotenv'), require('crypto-js/enc-hex'), require('crypto-js/sha256'), require('crypto'), require('socket.io-client'), require('rxjs'), require('child_process'), require('jszip'), require('prettier'), require('prettier/parser-html'), require('papaparse'), require('crypto-js'), require('mime-types'), require('glob-promise'), require('moment'), require('express'), require('http'), require('socket.io'), require('swagger-jsdoc'), require('swagger-ui-express'), require('@anthropic-ai/sdk'), require('@azure/openai'), require('openai'), require('@mozilla/readability'), require('jsdom'), require('showdown')) :
3
+ typeof define === 'function' && define.amd ? define(['exports', 'colors', 'commander', 'spacetrim', 'waitasecond', 'prompts', 'path', 'fs/promises', 'dotenv', 'crypto-js/enc-hex', 'crypto-js/sha256', 'crypto', 'socket.io-client', 'rxjs', 'child_process', 'jszip', 'prettier', 'prettier/parser-html', 'papaparse', 'crypto-js', 'mime-types', 'glob-promise', 'moment', 'express', 'http', 'socket.io', 'swagger-jsdoc', 'swagger-ui-express', '@anthropic-ai/sdk', '@azure/openai', 'openai', '@mozilla/readability', 'jsdom', 'showdown'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["promptbook-cli"] = {}, global.colors, global.commander, global.spaceTrim, global.waitasecond, global.prompts, global.path, global.promises, global.dotenv, global.hexEncoder, global.sha256, global.crypto, global.socket_ioClient, global.rxjs, global.child_process, global.JSZip, global.prettier, global.parserHtml, global.papaparse, global.cryptoJs, global.mimeTypes, global.glob, global.moment, global.express, global.http, global.socket_io, global.swaggerJsdoc, global.swaggerUi, global.Anthropic, global.openai, global.OpenAI, global.readability, global.jsdom, global.showdown));
5
+ })(this, (function (exports, colors, commander, spaceTrim, waitasecond, prompts, path, promises, dotenv, hexEncoder, sha256, crypto, socket_ioClient, rxjs, child_process, JSZip, prettier, parserHtml, papaparse, cryptoJs, mimeTypes, glob, moment, express, http, socket_io, swaggerJsdoc, swaggerUi, Anthropic, openai, OpenAI, readability, jsdom, showdown) { 'use strict';
6
6
 
7
7
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
8
8
 
@@ -27,16 +27,18 @@
27
27
  var colors__default = /*#__PURE__*/_interopDefaultLegacy(colors);
28
28
  var commander__default = /*#__PURE__*/_interopDefaultLegacy(commander);
29
29
  var spaceTrim__default = /*#__PURE__*/_interopDefaultLegacy(spaceTrim);
30
+ var prompts__default = /*#__PURE__*/_interopDefaultLegacy(prompts);
31
+ var dotenv__namespace = /*#__PURE__*/_interopNamespace(dotenv);
30
32
  var hexEncoder__default = /*#__PURE__*/_interopDefaultLegacy(hexEncoder);
31
33
  var sha256__default = /*#__PURE__*/_interopDefaultLegacy(sha256);
32
- var dotenv__namespace = /*#__PURE__*/_interopNamespace(dotenv);
33
34
  var JSZip__default = /*#__PURE__*/_interopDefaultLegacy(JSZip);
34
35
  var parserHtml__default = /*#__PURE__*/_interopDefaultLegacy(parserHtml);
35
36
  var glob__default = /*#__PURE__*/_interopDefaultLegacy(glob);
36
- var prompts__default = /*#__PURE__*/_interopDefaultLegacy(prompts);
37
37
  var moment__default = /*#__PURE__*/_interopDefaultLegacy(moment);
38
38
  var express__default = /*#__PURE__*/_interopDefaultLegacy(express);
39
39
  var http__default = /*#__PURE__*/_interopDefaultLegacy(http);
40
+ var swaggerJsdoc__default = /*#__PURE__*/_interopDefaultLegacy(swaggerJsdoc);
41
+ var swaggerUi__default = /*#__PURE__*/_interopDefaultLegacy(swaggerUi);
40
42
  var Anthropic__default = /*#__PURE__*/_interopDefaultLegacy(Anthropic);
41
43
  var OpenAI__default = /*#__PURE__*/_interopDefaultLegacy(OpenAI);
42
44
 
@@ -54,7 +56,7 @@
54
56
  * @generated
55
57
  * @see https://github.com/webgptorg/promptbook
56
58
  */
57
- const PROMPTBOOK_ENGINE_VERSION = '0.88.0';
59
+ const PROMPTBOOK_ENGINE_VERSION = '0.89.0-11';
58
60
  /**
59
61
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
60
62
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -223,6 +225,7 @@
223
225
  */
224
226
  const DEFAULT_BOOKS_DIRNAME = './books';
225
227
  // <- TODO: [🕝] Make also `BOOKS_DIRNAME_ALTERNATIVES`
228
+ // TODO: !!!!!! Just .promptbook dir, hardocode others
226
229
  /**
227
230
  * Where to store the temporary downloads
228
231
  *
@@ -247,6 +250,21 @@
247
250
  * @public exported from `@promptbook/core`
248
251
  */
249
252
  const DEFAULT_SCRAPE_CACHE_DIRNAME = './.promptbook/scrape-cache';
253
+ /**
254
+ * Id of application for the CLI when using remote server
255
+ *
256
+ * @public exported from `@promptbook/core`
257
+ */
258
+ const CLI_APP_ID = 'cli';
259
+ /*
260
+ TODO: [🌃]
261
+ /**
262
+ * Id of application for the wizzard when using remote server
263
+ *
264
+ * @public exported from `@promptbook/core`
265
+ * /
266
+ ex-port const WIZZARD_APP_ID: string_app_id = 'wizzard';
267
+ */
250
268
  /**
251
269
  * The name of the builded pipeline collection made by CLI `ptbk make` and for lookup in `createCollectionFromDirectory`
252
270
  *
@@ -267,13 +285,7 @@
267
285
  *
268
286
  * @public exported from `@promptbook/core`
269
287
  */
270
- const DEFAULT_REMOTE_URL = 'https://api.pavolhejny.com/';
271
- /**
272
- * @@@
273
- *
274
- * @public exported from `@promptbook/core`
275
- */
276
- const DEFAULT_REMOTE_URL_PATH = '/promptbook/socket.io';
288
+ const DEFAULT_REMOTE_SERVER_URL = 'https://api.pavolhejny.com/promptbook';
277
289
  // <- TODO: [🧜‍♂️]
278
290
  /**
279
291
  * @@@
@@ -315,7 +327,7 @@
315
327
  true);
316
328
  /**
317
329
  * Note: [💞] Ignore a discrepancy between file name and entity name
318
- * TODO: [🧠][🧜‍♂️] Maybe join remoteUrl and path into single value
330
+ * TODO: [🧠][🧜‍♂️] Maybe join remoteServerUrl and path into single value
319
331
  */
320
332
 
321
333
  /**
@@ -493,7 +505,8 @@
493
505
  helloCommand.alias('hi');
494
506
  helloCommand.argument('[name]', 'Your name', 'Paul');
495
507
  helloCommand.option('-g, --greeting <greeting>', `Greeting`, 'Hello');
496
- helloCommand.action(handleActionErrors(async (name, { greeting }) => {
508
+ helloCommand.action(handleActionErrors(async (name, cliOptions) => {
509
+ const { greeting } = cliOptions;
497
510
  console.info(colors__default["default"].cyan(`${greeting} ${name}`));
498
511
  await waitasecond.forTime(1000);
499
512
  console.info(colors__default["default"].rainbow(`Nice to meet you!`));
@@ -507,40 +520,27 @@
507
520
  */
508
521
 
509
522
  /**
510
- * Just marks a place of place where should be something implemented
511
- * No side effects.
512
- *
513
- * Note: It can be usefull suppressing eslint errors of unused variables
523
+ * This error type indicates that some part of the code is not implemented yet
514
524
  *
515
- * @param value any values
516
- * @returns void
517
- * @private within the repository
525
+ * @public exported from `@promptbook/core`
518
526
  */
519
- function TODO_USE(...value) {
520
- }
527
+ class NotYetImplementedError extends Error {
528
+ constructor(message) {
529
+ super(spaceTrim.spaceTrim((block) => `
530
+ ${block(message)}
521
531
 
522
- /**
523
- * @@@
524
- *
525
- * @public exported from `@promptbook/node`
526
- */
527
- function $provideFilesystemForNode(options) {
528
- if (!$isRunningInNode()) {
529
- throw new EnvironmentMismatchError('Function `$provideFilesystemForNode` works only in Node.js environment');
532
+ Note: This feature is not implemented yet but it will be soon.
533
+
534
+ If you want speed up the implementation or just read more, look here:
535
+ https://github.com/webgptorg/promptbook
536
+
537
+ Or contact us on pavol@ptbk.io
538
+
539
+ `));
540
+ this.name = 'NotYetImplementedError';
541
+ Object.setPrototypeOf(this, NotYetImplementedError.prototype);
530
542
  }
531
- return {
532
- stat: promises.stat,
533
- access: promises.access,
534
- constants: promises.constants,
535
- readFile: promises.readFile,
536
- writeFile: promises.writeFile,
537
- readdir: promises.readdir,
538
- mkdir: promises.mkdir,
539
- };
540
543
  }
541
- /**
542
- * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
543
- */
544
544
 
545
545
  /**
546
546
  * Make error report URL for the given error
@@ -611,375 +611,362 @@
611
611
  }
612
612
 
613
613
  /**
614
- * Orders JSON object by keys
614
+ * @@@
615
615
  *
616
- * @returns The same type of object as the input re-ordered
617
- * @public exported from `@promptbook/utils`
616
+ * Note: `$` is used to indicate that this function is not a pure function - it access global scope
617
+ *
618
+ * @private internal function of `$Register`
618
619
  */
619
- function orderJson(options) {
620
- const { value, order } = options;
621
- const orderedValue = {
622
- ...(order === undefined ? {} : Object.fromEntries(order.map((key) => [key, undefined]))),
623
- ...value,
624
- };
625
- return orderedValue;
620
+ function $getGlobalScope() {
621
+ return Function('return this')();
626
622
  }
627
623
 
628
624
  /**
629
- * Freezes the given object and all its nested objects recursively
630
- *
631
- * Note: `$` is used to indicate that this function is not a pure function - it mutates given object
632
- * Note: This function mutates the object and returns the original (but mutated-deep-freezed) object
625
+ * @@@
633
626
  *
634
- * @returns The same object as the input, but deeply frozen
627
+ * @param text @@@
628
+ * @returns @@@
629
+ * @example 'HELLO_WORLD'
630
+ * @example 'I_LOVE_PROMPTBOOK'
635
631
  * @public exported from `@promptbook/utils`
636
632
  */
637
- function $deepFreeze(objectValue) {
638
- if (Array.isArray(objectValue)) {
639
- return Object.freeze(objectValue.map((item) => $deepFreeze(item)));
640
- }
641
- const propertyNames = Object.getOwnPropertyNames(objectValue);
642
- for (const propertyName of propertyNames) {
643
- const value = objectValue[propertyName];
644
- if (value && typeof value === 'object') {
645
- $deepFreeze(value);
633
+ function normalizeTo_SCREAMING_CASE(text) {
634
+ let charType;
635
+ let lastCharType = 'OTHER';
636
+ let normalizedName = '';
637
+ for (const char of text) {
638
+ let normalizedChar;
639
+ if (/^[a-z]$/.test(char)) {
640
+ charType = 'LOWERCASE';
641
+ normalizedChar = char.toUpperCase();
642
+ }
643
+ else if (/^[A-Z]$/.test(char)) {
644
+ charType = 'UPPERCASE';
645
+ normalizedChar = char;
646
+ }
647
+ else if (/^[0-9]$/.test(char)) {
648
+ charType = 'NUMBER';
649
+ normalizedChar = char;
650
+ }
651
+ else {
652
+ charType = 'OTHER';
653
+ normalizedChar = '_';
654
+ }
655
+ if (charType !== lastCharType &&
656
+ !(lastCharType === 'UPPERCASE' && charType === 'LOWERCASE') &&
657
+ !(lastCharType === 'NUMBER') &&
658
+ !(charType === 'NUMBER')) {
659
+ normalizedName += '_';
646
660
  }
661
+ normalizedName += normalizedChar;
662
+ lastCharType = charType;
647
663
  }
648
- Object.freeze(objectValue);
649
- return objectValue;
664
+ normalizedName = normalizedName.replace(/_+/g, '_');
665
+ normalizedName = normalizedName.replace(/_?\/_?/g, '/');
666
+ normalizedName = normalizedName.replace(/^_/, '');
667
+ normalizedName = normalizedName.replace(/_$/, '');
668
+ return normalizedName;
650
669
  }
651
670
  /**
652
- * TODO: [🧠] Is there a way how to meaningfully test this utility
671
+ * TODO: Tests
672
+ * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: 'Moje tabule' })).toEqual('/VtG7sR9rRJqwNEdM2/Moje tabule');
673
+ * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: 'ěščřžžýáíúů' })).toEqual('/VtG7sR9rRJqwNEdM2/escrzyaieuu');
674
+ * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: ' ahoj ' })).toEqual('/VtG7sR9rRJqwNEdM2/ahoj');
675
+ * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: ' ahoj_ahojAhoj ahoj ' })).toEqual('/VtG7sR9rRJqwNEdM2/ahoj-ahoj-ahoj-ahoj');
676
+ * TODO: [🌺] Use some intermediate util splitWords
653
677
  */
654
678
 
655
679
  /**
656
- * Checks if the value is [🚉] serializable as JSON
657
- * If not, throws an UnexpectedError with a rich error message and tracking
658
- *
659
- * - Almost all primitives are serializable BUT:
660
- * - `undefined` is not serializable
661
- * - `NaN` is not serializable
662
- * - Objects and arrays are serializable if all their properties are serializable
663
- * - Functions are not serializable
664
- * - Circular references are not serializable
665
- * - `Date` objects are not serializable
666
- * - `Map` and `Set` objects are not serializable
667
- * - `RegExp` objects are not serializable
668
- * - `Error` objects are not serializable
669
- * - `Symbol` objects are not serializable
670
- * - And much more...
680
+ * @@@
671
681
  *
672
- * @throws UnexpectedError if the value is not serializable as JSON
682
+ * @param text @@@
683
+ * @returns @@@
684
+ * @example 'hello_world'
685
+ * @example 'i_love_promptbook'
673
686
  * @public exported from `@promptbook/utils`
674
687
  */
675
- function checkSerializableAsJson(options) {
676
- const { value, name, message } = options;
677
- if (value === undefined) {
678
- throw new UnexpectedError(`${name} is undefined`);
679
- }
680
- else if (value === null) {
681
- return;
682
- }
683
- else if (typeof value === 'boolean') {
684
- return;
685
- }
686
- else if (typeof value === 'number' && !isNaN(value)) {
687
- return;
688
- }
689
- else if (typeof value === 'string') {
690
- return;
691
- }
692
- else if (typeof value === 'symbol') {
693
- throw new UnexpectedError(`${name} is symbol`);
694
- }
695
- else if (typeof value === 'function') {
696
- throw new UnexpectedError(`${name} is function`);
697
- }
698
- else if (typeof value === 'object' && Array.isArray(value)) {
699
- for (let i = 0; i < value.length; i++) {
700
- checkSerializableAsJson({ name: `${name}[${i}]`, value: value[i], message });
701
- }
702
- }
703
- else if (typeof value === 'object') {
704
- if (value instanceof Date) {
705
- throw new UnexpectedError(spaceTrim__default["default"]((block) => `
706
- \`${name}\` is Date
707
-
708
- Use \`string_date_iso8601\` instead
688
+ function normalizeTo_snake_case(text) {
689
+ return normalizeTo_SCREAMING_CASE(text).toLowerCase();
690
+ }
709
691
 
710
- Additional message for \`${name}\`:
711
- ${block(message || '(nothing)')}
712
- `));
692
+ /**
693
+ * Register is @@@
694
+ *
695
+ * Note: `$` is used to indicate that this function is not a pure function - it accesses and adds variables in global scope.
696
+ *
697
+ * @private internal utility, exported are only signleton instances of this class
698
+ */
699
+ class $Register {
700
+ constructor(registerName) {
701
+ this.registerName = registerName;
702
+ const storageName = `_promptbook_${normalizeTo_snake_case(registerName)}`;
703
+ const globalScope = $getGlobalScope();
704
+ if (globalScope[storageName] === undefined) {
705
+ globalScope[storageName] = [];
713
706
  }
714
- else if (value instanceof Map) {
715
- throw new UnexpectedError(`${name} is Map`);
707
+ else if (!Array.isArray(globalScope[storageName])) {
708
+ throw new UnexpectedError(`Expected (global) ${storageName} to be an array, but got ${typeof globalScope[storageName]}`);
716
709
  }
717
- else if (value instanceof Set) {
718
- throw new UnexpectedError(`${name} is Set`);
710
+ this.storage = globalScope[storageName];
711
+ }
712
+ list() {
713
+ // <- TODO: ReadonlyDeep<ReadonlyArray<TRegistered>>
714
+ return this.storage;
715
+ }
716
+ register(registered) {
717
+ const { packageName, className } = registered;
718
+ const existingRegistrationIndex = this.storage.findIndex((item) => item.packageName === packageName && item.className === className);
719
+ const existingRegistration = this.storage[existingRegistrationIndex];
720
+ if (!existingRegistration) {
721
+ this.storage.push(registered);
719
722
  }
720
- else if (value instanceof RegExp) {
721
- throw new UnexpectedError(`${name} is RegExp`);
723
+ else {
724
+ this.storage[existingRegistrationIndex] = registered;
722
725
  }
723
- else if (value instanceof Error) {
724
- throw new UnexpectedError(spaceTrim__default["default"]((block) => `
725
- \`${name}\` is unserialized Error
726
-
727
- Use function \`serializeError\`
728
-
729
- Additional message for \`${name}\`:
730
- ${block(message || '(nothing)')}
731
-
732
- `));
733
- }
734
- else {
735
- for (const [subName, subValue] of Object.entries(value)) {
736
- if (subValue === undefined) {
737
- // Note: undefined in object is serializable - it is just omited
738
- continue;
739
- }
740
- checkSerializableAsJson({ name: `${name}.${subName}`, value: subValue, message });
741
- }
742
- try {
743
- JSON.stringify(value); // <- TODO: [0]
744
- }
745
- catch (error) {
746
- if (!(error instanceof Error)) {
747
- throw error;
748
- }
749
- throw new UnexpectedError(spaceTrim__default["default"]((block) => `
750
- \`${name}\` is not serializable
751
-
752
- ${block(error.stack || error.message)}
753
-
754
- Additional message for \`${name}\`:
755
- ${block(message || '(nothing)')}
756
- `));
757
- }
758
- /*
759
- TODO: [0] Is there some more elegant way to check circular references?
760
- const seen = new Set();
761
- const stack = [{ value }];
762
- while (stack.length > 0) {
763
- const { value } = stack.pop()!;
764
- if (typeof value === 'object' && value !== null) {
765
- if (seen.has(value)) {
766
- throw new UnexpectedError(`${name} has circular reference`);
767
- }
768
- seen.add(value);
769
- if (Array.isArray(value)) {
770
- stack.push(...value.map((value) => ({ value })));
771
- } else {
772
- stack.push(...Object.values(value).map((value) => ({ value })));
773
- }
774
- }
775
- }
776
- */
777
- return;
778
- }
779
- }
780
- else {
781
- throw new UnexpectedError(spaceTrim__default["default"]((block) => `
782
- \`${name}\` is unknown type
783
-
784
- Additional message for \`${name}\`:
785
- ${block(message || '(nothing)')}
786
- `));
787
- }
788
- }
789
- /**
790
- * TODO: Can be return type more type-safe? like `asserts options.value is JsonValue`
791
- * TODO: [🧠][main] !!3 In-memory cache of same values to prevent multiple checks
792
- * Note: [🐠] This is how `checkSerializableAsJson` + `isSerializableAsJson` together can just retun true/false or rich error message
793
- */
726
+ return {
727
+ registerName: this.registerName,
728
+ packageName,
729
+ className,
730
+ get isDestroyed() {
731
+ return false;
732
+ },
733
+ destroy() {
734
+ throw new NotYetImplementedError(`Registration to ${this.registerName} is permanent in this version of Promptbook`);
735
+ },
736
+ };
737
+ }
738
+ }
794
739
 
795
740
  /**
796
741
  * @@@
797
742
  *
798
- * @public exported from `@promptbook/utils`
799
- */
800
- function deepClone(objectValue) {
801
- return JSON.parse(JSON.stringify(objectValue));
802
- /*
803
- TODO: [🧠] Is there a better implementation?
804
- > const propertyNames = Object.getOwnPropertyNames(objectValue);
805
- > for (const propertyName of propertyNames) {
806
- > const value = (objectValue as really_any)[propertyName];
807
- > if (value && typeof value === 'object') {
808
- > deepClone(value);
809
- > }
810
- > }
811
- > return Object.assign({}, objectValue);
812
- */
813
- }
814
- /**
815
- * TODO: [🧠] Is there a way how to meaningfully test this utility
816
- */
817
-
818
- /**
819
- * Utility to export a JSON object from a function
820
- *
821
- * 1) Checks if the value is serializable as JSON
822
- * 2) Makes a deep clone of the object
823
- * 2) Orders the object properties
824
- * 2) Deeply freezes the cloned object
825
- *
826
- * Note: This function does not mutates the given object
827
- *
828
- * @returns The same type of object as the input but read-only and re-ordered
829
- * @public exported from `@promptbook/utils`
830
- */
831
- function exportJson(options) {
832
- const { name, value, order, message } = options;
833
- checkSerializableAsJson({ name, value, message });
834
- const orderedValue =
835
- // TODO: Fix error "Type instantiation is excessively deep and possibly infinite."
836
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
837
- // @ts-ignore
838
- order === undefined
839
- ? deepClone(value)
840
- : orderJson({
841
- value: value,
842
- // <- Note: checkSerializableAsJson asserts that the value is serializable as JSON
843
- order: order,
844
- });
845
- $deepFreeze(orderedValue);
846
- return orderedValue;
847
- }
848
- /**
849
- * TODO: [🧠] Is there a way how to meaningfully test this utility
850
- */
851
-
852
- /**
853
- * Order of keys in the pipeline JSON
854
- *
743
+ * Note: `$` is used to indicate that this interacts with the global scope
744
+ * @singleton Only one instance of each register is created per build, but thare can be more @@@
855
745
  * @public exported from `@promptbook/core`
856
746
  */
857
- const ORDER_OF_PIPELINE_JSON = [
858
- // Note: [🍙] In this order will be pipeline serialized
859
- 'title',
860
- 'pipelineUrl',
861
- 'bookVersion',
862
- 'description',
863
- 'formfactorName',
864
- 'parameters',
865
- 'tasks',
866
- 'personas',
867
- 'preparations',
868
- 'knowledgeSources',
869
- 'knowledgePieces',
870
- 'sources', // <- TODO: [🧠] Where should the `sources` be
871
- ];
872
- /**
873
- * Nonce which is used for replacing things in strings
874
- *
875
- * @private within the repository
876
- */
877
- const REPLACING_NONCE = 'ptbkauk42kV2dzao34faw7FudQUHYPtW';
747
+ const $llmToolsMetadataRegister = new $Register('llm_tools_metadata');
878
748
  /**
879
- * @@@
880
- *
881
- * @private within the repository
749
+ * TODO: [®] DRY Register logic
882
750
  */
883
- const RESERVED_PARAMETER_MISSING_VALUE = 'MISSING-' + REPLACING_NONCE;
751
+
884
752
  /**
885
753
  * @@@
886
754
  *
887
- * @private within the repository
888
- */
889
- const RESERVED_PARAMETER_RESTRICTED = 'RESTRICTED-' + REPLACING_NONCE;
890
- /**
891
- * The names of the parameters that are reserved for special purposes
892
- *
755
+ * Note: `$` is used to indicate that this interacts with the global scope
756
+ * @singleton Only one instance of each register is created per build, but thare can be more @@@
893
757
  * @public exported from `@promptbook/core`
894
758
  */
895
- const RESERVED_PARAMETER_NAMES = exportJson({
896
- name: 'RESERVED_PARAMETER_NAMES',
897
- message: `The names of the parameters that are reserved for special purposes`,
898
- value: [
899
- 'content',
900
- 'context',
901
- 'knowledge',
902
- 'examples',
903
- 'modelName',
904
- 'currentDate',
905
- // <- TODO: list here all command names
906
- // <- TODO: Add more like 'date', 'modelName',...
907
- // <- TODO: Add [emoji] + instructions ACRY when adding new reserved parameter
908
- ],
909
- });
759
+ const $llmToolsRegister = new $Register('llm_execution_tools_constructors');
910
760
  /**
911
- * Note: [💞] Ignore a discrepancy between file name and entity name
761
+ * TODO: [®] DRY Register logic
912
762
  */
913
763
 
914
- // <- TODO: Auto convert to type `import { ... } from 'type-fest';`
915
764
  /**
916
- * Tests if the value is [🚉] serializable as JSON
765
+ * Path to the `.env` file which was used to configure LLM tools
917
766
  *
918
- * - Almost all primitives are serializable BUT:
919
- * - `undefined` is not serializable
920
- * - `NaN` is not serializable
921
- * - Objects and arrays are serializable if all their properties are serializable
922
- * - Functions are not serializable
923
- * - Circular references are not serializable
924
- * - `Date` objects are not serializable
925
- * - `Map` and `Set` objects are not serializable
926
- * - `RegExp` objects are not serializable
927
- * - `Error` objects are not serializable
928
- * - `Symbol` objects are not serializable
929
- * - And much more...
767
+ * Note: `$` is used to indicate that this variable is changed by side effect in `$provideLlmToolsConfigurationFromEnv` through `$setUsedEnvFilename`
768
+ */
769
+ let $usedEnvFilename = null;
770
+ /**
771
+ * Pass the `.env` file which was used to configure LLM tools
930
772
  *
773
+ * Note: `$` is used to indicate that this variable is making side effect
931
774
  *
932
- * @public exported from `@promptbook/utils`
775
+ * @private internal log of `$provideLlmToolsConfigurationFromEnv` and `$registeredLlmToolsMessage`
933
776
  */
934
- function isSerializableAsJson(value) {
935
- try {
936
- checkSerializableAsJson({ value });
937
- return true;
938
- }
939
- catch (error) {
940
- return false;
941
- }
777
+ function $setUsedEnvFilename(filepath) {
778
+ $usedEnvFilename = filepath;
942
779
  }
943
780
  /**
944
- * TODO: [🧠][main] !!3 In-memory cache of same values to prevent multiple checks
945
- * TODO: [🧠][💺] Can be done this on type-level?
946
- */
947
-
948
- /**
949
- * Stringify the PipelineJson with proper formatting
781
+ * Creates a message with all registered LLM tools
950
782
  *
951
- * Note: [0] It can be used for more JSON types like whole collection of pipelines, single knowledge piece, etc.
952
- * Note: In contrast to JSON.stringify, this function ensures that **embedding index** is on single line
783
+ * Note: This function is used to create a (error) message when there is no constructor for some LLM provider
953
784
  *
954
- * @public exported from `@promptbook/editable`
785
+ * @private internal function of `createLlmToolsFromConfiguration` and `$provideLlmToolsFromEnv`
955
786
  */
956
- function stringifyPipelineJson(pipeline) {
957
- if (!isSerializableAsJson(pipeline)) {
958
- throw new UnexpectedError(spaceTrim__default["default"](`
959
- Cannot stringify the pipeline, because it is not serializable as JSON
960
-
961
- There can be multiple reasons:
962
- 1) The pipeline contains circular references
963
- 2) It is not a valid PipelineJson
964
- `));
787
+ function $registeredLlmToolsMessage() {
788
+ let env;
789
+ if ($isRunningInNode()) {
790
+ env = process.env;
791
+ // <- TODO: [⚛] Some DRY way how to get to `process.env` and pass it into functions - ACRY search for `env`
965
792
  }
966
- let pipelineJsonStringified = JSON.stringify(pipeline, null, 4);
967
- for (let i = 0; i < LOOP_LIMIT; i++) {
968
- pipelineJsonStringified = pipelineJsonStringified.replace(/(-?0\.\d+),[\n\s]+(-?0\.\d+)/gms, `$1${REPLACING_NONCE}$2`);
793
+ else {
794
+ env = {};
969
795
  }
970
- pipelineJsonStringified = pipelineJsonStringified.split(REPLACING_NONCE).join(', ');
971
- pipelineJsonStringified += '\n';
972
- return pipelineJsonStringified;
973
- }
974
- /**
975
- * TODO: [🐝] Not Working propperly @see https://promptbook.studio/examples/mixed-knowledge.book
976
- * TODO: [🧠][0] Maybe rename to `stringifyPipelineJson`, `stringifyIndexedJson`,...
977
- * TODO: [🧠] Maybe more elegant solution than replacing via regex
978
- * TODO: [🍙] Make some standard order of json properties
979
- */
980
-
981
- /**
982
- * Checks if the file exists
796
+ /**
797
+ * Mixes registered LLM tools from $llmToolsMetadataRegister and $llmToolsRegister
798
+ */
799
+ const all = [];
800
+ for (const { title, packageName, className, envVariables } of $llmToolsMetadataRegister.list()) {
801
+ if (all.some((item) => item.packageName === packageName && item.className === className)) {
802
+ continue;
803
+ }
804
+ all.push({ title, packageName, className, envVariables });
805
+ }
806
+ for (const { packageName, className } of $llmToolsRegister.list()) {
807
+ if (all.some((item) => item.packageName === packageName && item.className === className)) {
808
+ continue;
809
+ }
810
+ all.push({ packageName, className });
811
+ }
812
+ const metadata = all.map((metadata) => {
813
+ var _a, _b;
814
+ const isMetadataAviailable = $llmToolsMetadataRegister
815
+ .list()
816
+ .find(({ packageName, className }) => metadata.packageName === packageName && metadata.className === className);
817
+ const isInstalled = $llmToolsRegister
818
+ .list()
819
+ .find(({ packageName, className }) => metadata.packageName === packageName && metadata.className === className);
820
+ const isFullyConfigured = ((_a = metadata.envVariables) === null || _a === void 0 ? void 0 : _a.every((envVariableName) => env[envVariableName] !== undefined)) || false;
821
+ const isPartiallyConfigured = ((_b = metadata.envVariables) === null || _b === void 0 ? void 0 : _b.some((envVariableName) => env[envVariableName] !== undefined)) || false;
822
+ // <- Note: [🗨]
823
+ return { ...metadata, isMetadataAviailable, isInstalled, isFullyConfigured, isPartiallyConfigured };
824
+ });
825
+ const usedEnvMessage = $usedEnvFilename === null ? `Unknown \`.env\` file` : `Used \`.env\` file:\n${$usedEnvFilename}`;
826
+ if (metadata.length === 0) {
827
+ return spaceTrim__default["default"]((block) => `
828
+ No LLM providers are available.
829
+
830
+ ${block(usedEnvMessage)}
831
+ `);
832
+ }
833
+ return spaceTrim__default["default"]((block) => `
834
+
835
+ ${block(usedEnvMessage)}
836
+
837
+ Relevant environment variables:
838
+ ${block(Object.keys(env)
839
+ .filter((envVariableName) => metadata.some(({ envVariables }) => envVariables === null || envVariables === void 0 ? void 0 : envVariables.includes(envVariableName)))
840
+ .map((envVariableName) => `- \`${envVariableName}\``)
841
+ .join('\n'))}
842
+
843
+ Available LLM providers are:
844
+ ${block(metadata
845
+ .map(({ title, packageName, className, envVariables, isMetadataAviailable, isInstalled, isFullyConfigured, isPartiallyConfigured, }, i) => {
846
+ const morePieces = [];
847
+ if (just(false)) ;
848
+ else if (!isMetadataAviailable && !isInstalled) {
849
+ // TODO: [�][�] Maybe do allow to do auto-install if package not registered and not found
850
+ morePieces.push(`Not installed and no metadata, looks like a unexpected behavior`);
851
+ }
852
+ else if (isMetadataAviailable && !isInstalled) {
853
+ // TODO: [�][�]
854
+ morePieces.push(`Not installed`);
855
+ }
856
+ else if (!isMetadataAviailable && isInstalled) {
857
+ morePieces.push(`No metadata but installed, looks like a unexpected behavior`);
858
+ }
859
+ else if (isMetadataAviailable && isInstalled) {
860
+ morePieces.push(`Installed`);
861
+ }
862
+ else {
863
+ morePieces.push(`unknown state, looks like a unexpected behavior`);
864
+ } /* not else */
865
+ if (isFullyConfigured) {
866
+ morePieces.push(`Configured`);
867
+ }
868
+ else if (isPartiallyConfigured) {
869
+ morePieces.push(`Partially confugured, missing ${envVariables === null || envVariables === void 0 ? void 0 : envVariables.filter((envVariable) => env[envVariable] === undefined).join(' + ')}`);
870
+ }
871
+ else {
872
+ if (envVariables !== null) {
873
+ morePieces.push(`Not configured, to configure set env ${envVariables === null || envVariables === void 0 ? void 0 : envVariables.join(' + ')}`);
874
+ }
875
+ else {
876
+ morePieces.push(`Not configured`); // <- Note: Can not be configured via environment variables
877
+ }
878
+ }
879
+ let providerMessage = spaceTrim__default["default"](`
880
+ ${i + 1}) **${title}** \`${className}\` from \`${packageName}\`
881
+ ${morePieces.join('; ')}
882
+ `);
883
+ if ($isRunningInNode) {
884
+ if (isInstalled && isFullyConfigured) {
885
+ providerMessage = colors__default["default"].green(providerMessage);
886
+ }
887
+ else if (isInstalled && isPartiallyConfigured) {
888
+ providerMessage = colors__default["default"].yellow(providerMessage);
889
+ }
890
+ else {
891
+ providerMessage = colors__default["default"].gray(providerMessage);
892
+ }
893
+ }
894
+ return providerMessage;
895
+ })
896
+ .join('\n'))}
897
+ `);
898
+ }
899
+ /**
900
+ * TODO: [®] DRY Register logic
901
+ * TODO: [🧠][⚛] Maybe pass env as argument
902
+ */
903
+
904
+ /**
905
+ * Just says that the variable is not used but should be kept
906
+ * No side effects.
907
+ *
908
+ * Note: It can be usefull for:
909
+ *
910
+ * 1) Suppressing eager optimization of unused imports
911
+ * 2) Suppressing eslint errors of unused variables in the tests
912
+ * 3) Keeping the type of the variable for type testing
913
+ *
914
+ * @param value any values
915
+ * @returns void
916
+ * @private within the repository
917
+ */
918
+ function keepUnused(...valuesToKeep) {
919
+ }
920
+
921
+ /**
922
+ * Just says that the variable is not used directlys but should be kept because the existence of the variable is important
923
+ *
924
+ * @param value any values
925
+ * @returns void
926
+ * @private within the repository
927
+ */
928
+ function $sideEffect(...sideEffectSubjects) {
929
+ keepUnused(...sideEffectSubjects);
930
+ }
931
+
932
+ /**
933
+ * Just marks a place of place where should be something implemented
934
+ * No side effects.
935
+ *
936
+ * Note: It can be usefull suppressing eslint errors of unused variables
937
+ *
938
+ * @param value any values
939
+ * @returns void
940
+ * @private within the repository
941
+ */
942
+ function TODO_USE(...value) {
943
+ }
944
+
945
+ /**
946
+ * @@@
947
+ *
948
+ * @public exported from `@promptbook/node`
949
+ */
950
+ function $provideFilesystemForNode(options) {
951
+ if (!$isRunningInNode()) {
952
+ throw new EnvironmentMismatchError('Function `$provideFilesystemForNode` works only in Node.js environment');
953
+ }
954
+ return {
955
+ stat: promises.stat,
956
+ access: promises.access,
957
+ constants: promises.constants,
958
+ readFile: promises.readFile,
959
+ writeFile: promises.writeFile,
960
+ readdir: promises.readdir,
961
+ mkdir: promises.mkdir,
962
+ };
963
+ }
964
+ /**
965
+ * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
966
+ */
967
+
968
+ /**
969
+ * Checks if the file exists
983
970
  *
984
971
  * @private within the repository
985
972
  */
@@ -1004,99 +991,607 @@
1004
991
  */
1005
992
 
1006
993
  /**
1007
- * Removes emojis from a string and fix whitespaces
1008
- *
1009
- * @param text with emojis
1010
- * @returns text without emojis
1011
- * @public exported from `@promptbook/utils`
1012
- */
1013
- function removeEmojis(text) {
1014
- // Replace emojis (and also ZWJ sequence) with hyphens
1015
- text = text.replace(/(\p{Extended_Pictographic})\p{Modifier_Symbol}/gu, '$1');
1016
- text = text.replace(/(\p{Extended_Pictographic})[\u{FE00}-\u{FE0F}]/gu, '$1');
1017
- text = text.replace(/(\p{Extended_Pictographic})(\u{200D}\p{Extended_Pictographic})*/gu, '$1');
1018
- text = text.replace(/\p{Extended_Pictographic}/gu, '');
1019
- return text;
1020
- }
1021
-
1022
- /**
1023
- * Tests if given string is valid URL.
994
+ * Determines if the given path is a root path.
1024
995
  *
1025
996
  * Note: This does not check if the file exists only if the path is valid
1026
997
  * @public exported from `@promptbook/utils`
1027
998
  */
1028
- function isValidFilePath(filename) {
1029
- if (typeof filename !== 'string') {
1030
- return false;
1031
- }
1032
- if (filename.split('\n').length > 1) {
1033
- return false;
1034
- }
1035
- if (filename.split(' ').length >
1036
- 5 /* <- TODO: [🧠][🈷] Make some better non-arbitrary way how to distinct filenames from informational texts */) {
1037
- return false;
1038
- }
1039
- const filenameSlashes = filename.split('\\').join('/');
1040
- // Absolute Unix path: /hello.txt
1041
- if (/^(\/)/i.test(filenameSlashes)) {
1042
- // console.log(filename, 'Absolute Unix path: /hello.txt');
1043
- return true;
1044
- }
1045
- // Absolute Windows path: /hello.txt
1046
- if (/^([A-Z]{1,2}:\/?)\//i.test(filenameSlashes)) {
1047
- // console.log(filename, 'Absolute Windows path: /hello.txt');
1048
- return true;
1049
- }
1050
- // Relative path: ./hello.txt
1051
- if (/^(\.\.?\/)+/i.test(filenameSlashes)) {
1052
- // console.log(filename, 'Relative path: ./hello.txt');
1053
- return true;
1054
- }
1055
- // Allow paths like foo/hello
1056
- if (/^[^/]+\/[^/]+/i.test(filenameSlashes)) {
1057
- // console.log(filename, 'Allow paths like foo/hello');
999
+ function isRootPath(value) {
1000
+ if (value === '/') {
1058
1001
  return true;
1059
1002
  }
1060
- // Allow paths like hello.book
1061
- if (/^[^/]+\.[^/]+$/i.test(filenameSlashes)) {
1062
- // console.log(filename, 'Allow paths like hello.book');
1003
+ if (/^[A-Z]:\\$/i.test(value)) {
1063
1004
  return true;
1064
1005
  }
1065
1006
  return false;
1066
1007
  }
1067
1008
  /**
1068
- * TODO: [🍏] Implement for MacOs
1009
+ * TODO: [🍏] Make for MacOS paths
1069
1010
  */
1070
1011
 
1071
1012
  /**
1072
- * Tests if given string is valid URL.
1013
+ * Provides the path to the `.env` file
1073
1014
  *
1074
- * Note: Dataurl are considered perfectly valid.
1075
- * Note: There are two simmilar functions:
1076
- * - `isValidUrl` which tests any URL
1077
- * - `isValidPipelineUrl` *(this one)* which tests just promptbook URL
1015
+ * Note: `$` is used to indicate that this function is not a pure function - it uses filesystem to access `.env` file
1078
1016
  *
1079
- * @public exported from `@promptbook/utils`
1017
+ * @private within the repository - for CLI utils
1080
1018
  */
1081
- function isValidUrl(url) {
1082
- if (typeof url !== 'string') {
1083
- return false;
1084
- }
1085
- try {
1086
- if (url.startsWith('blob:')) {
1087
- url = url.replace(/^blob:/, '');
1088
- }
1089
- const urlObject = new URL(url /* because fail is handled */);
1090
- if (!['http:', 'https:', 'data:'].includes(urlObject.protocol)) {
1091
- return false;
1092
- }
1093
- return true;
1094
- }
1095
- catch (error) {
1096
- return false;
1019
+ async function $provideEnvFilename() {
1020
+ if (!$isRunningInNode()) {
1021
+ throw new EnvironmentMismatchError('Function `$provideEnvFilename` works only in Node.js environment');
1097
1022
  }
1098
- }
1099
-
1023
+ const envFilePatterns = [
1024
+ '.env',
1025
+ '.env.test',
1026
+ '.env.local',
1027
+ '.env.development.local',
1028
+ '.env.development',
1029
+ '.env.production.local',
1030
+ '.env.production',
1031
+ '.env.prod.local',
1032
+ '.env.prod',
1033
+ // <- TODO: Maybe add more patterns
1034
+ ];
1035
+ let rootDirname = process.cwd();
1036
+ up_to_root: for (let i = 0; i < LOOP_LIMIT; i++) {
1037
+ for (const pattern of envFilePatterns) {
1038
+ const envFilename = path.join(rootDirname, pattern);
1039
+ if (await isFileExisting(envFilename, $provideFilesystemForNode())) {
1040
+ $setUsedEnvFilename(envFilename);
1041
+ return envFilename;
1042
+ }
1043
+ }
1044
+ if (isRootPath(rootDirname)) {
1045
+ break up_to_root;
1046
+ }
1047
+ // Note: If the directory does not exist, try the parent directory
1048
+ rootDirname = path.join(rootDirname, '..');
1049
+ }
1050
+ return null;
1051
+ }
1052
+ /**
1053
+ * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
1054
+ */
1055
+
1056
+ /**
1057
+ * Stores data in .env variables (Remove !!! nonce 1)
1058
+ *
1059
+ * Note: `$` is used to indicate that this function is not a pure function - it uses filesystem to access `.env` file and also writes to `process.env`
1060
+ *
1061
+ * @private within the repository - for CLI utils
1062
+ */
1063
+ class $EnvStorage {
1064
+ constructor() {
1065
+ this.envFilename = null;
1066
+ }
1067
+ async $provideOrCreateEnvFile() {
1068
+ if (this.envFilename !== null) {
1069
+ return this.envFilename;
1070
+ }
1071
+ let envFilename = await $provideEnvFilename();
1072
+ if (envFilename !== null) {
1073
+ this.envFilename = envFilename;
1074
+ return envFilename;
1075
+ }
1076
+ envFilename = path.join(process.cwd(), '.env');
1077
+ await promises.writeFile(envFilename, '# This file was initialized by Promptbook', 'utf-8');
1078
+ this.envFilename = envFilename;
1079
+ return envFilename;
1080
+ }
1081
+ transformKey(key) {
1082
+ return normalizeTo_SCREAMING_CASE(key);
1083
+ }
1084
+ /**
1085
+ * Returns the number of key/value pairs currently present in the list associated with the object.
1086
+ */
1087
+ get length() {
1088
+ throw new NotYetImplementedError('Method `$EnvStorage.length` not implemented.');
1089
+ }
1090
+ /**
1091
+ * Empties the list associated with the object of all key/value pairs, if there are any.
1092
+ */
1093
+ clear() {
1094
+ throw new NotYetImplementedError('Method `$EnvStorage.clear` not implemented.');
1095
+ }
1096
+ /**
1097
+ * Returns the current value associated with the given key, or null if the given key does not exist in the list associated with the object.
1098
+ */
1099
+ async getItem(key) {
1100
+ dotenv__namespace.config({ path: await this.$provideOrCreateEnvFile() });
1101
+ return process.env[this.transformKey(key)] || null;
1102
+ }
1103
+ /**
1104
+ * Returns the name of the nth key in the list, or null if n is greater than or equal to the number of key/value pairs in the object.
1105
+ */
1106
+ key(index) {
1107
+ throw new NotYetImplementedError('Method `$EnvStorage.key` not implemented.');
1108
+ }
1109
+ /**
1110
+ * Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.
1111
+ */
1112
+ async setItem(key, value) {
1113
+ const envFilename = await this.$provideOrCreateEnvFile();
1114
+ const envContent = await promises.readFile(envFilename, 'utf-8');
1115
+ const newEnvContent = spaceTrim__default["default"]((block) => `
1116
+ ${block(envContent)}
1117
+
1118
+ # Note: Added by Promptbook
1119
+ ${this.transformKey(key)}=${JSON.stringify(value)}
1120
+
1121
+ `);
1122
+ // <- TODO: !!! Add note and use spacetrim
1123
+ promises.writeFile(envFilename, newEnvContent, 'utf-8');
1124
+ }
1125
+ /**
1126
+ * Removes the key/value pair with the given key from the list associated with the object, if a key/value pair with the given key exists.
1127
+ */
1128
+ removeItem(key) {
1129
+ throw new NotYetImplementedError('Method `$EnvStorage.removeItem` not implemented.');
1130
+ }
1131
+ }
1132
+
1133
+ /**
1134
+ * Orders JSON object by keys
1135
+ *
1136
+ * @returns The same type of object as the input re-ordered
1137
+ * @public exported from `@promptbook/utils`
1138
+ */
1139
+ function orderJson(options) {
1140
+ const { value, order } = options;
1141
+ const orderedValue = {
1142
+ ...(order === undefined ? {} : Object.fromEntries(order.map((key) => [key, undefined]))),
1143
+ ...value,
1144
+ };
1145
+ return orderedValue;
1146
+ }
1147
+
1148
+ /**
1149
+ * Freezes the given object and all its nested objects recursively
1150
+ *
1151
+ * Note: `$` is used to indicate that this function is not a pure function - it mutates given object
1152
+ * Note: This function mutates the object and returns the original (but mutated-deep-freezed) object
1153
+ *
1154
+ * @returns The same object as the input, but deeply frozen
1155
+ * @public exported from `@promptbook/utils`
1156
+ */
1157
+ function $deepFreeze(objectValue) {
1158
+ if (Array.isArray(objectValue)) {
1159
+ return Object.freeze(objectValue.map((item) => $deepFreeze(item)));
1160
+ }
1161
+ const propertyNames = Object.getOwnPropertyNames(objectValue);
1162
+ for (const propertyName of propertyNames) {
1163
+ const value = objectValue[propertyName];
1164
+ if (value && typeof value === 'object') {
1165
+ $deepFreeze(value);
1166
+ }
1167
+ }
1168
+ Object.freeze(objectValue);
1169
+ return objectValue;
1170
+ }
1171
+ /**
1172
+ * TODO: [🧠] Is there a way how to meaningfully test this utility
1173
+ */
1174
+
1175
+ /**
1176
+ * Checks if the value is [🚉] serializable as JSON
1177
+ * If not, throws an UnexpectedError with a rich error message and tracking
1178
+ *
1179
+ * - Almost all primitives are serializable BUT:
1180
+ * - `undefined` is not serializable
1181
+ * - `NaN` is not serializable
1182
+ * - Objects and arrays are serializable if all their properties are serializable
1183
+ * - Functions are not serializable
1184
+ * - Circular references are not serializable
1185
+ * - `Date` objects are not serializable
1186
+ * - `Map` and `Set` objects are not serializable
1187
+ * - `RegExp` objects are not serializable
1188
+ * - `Error` objects are not serializable
1189
+ * - `Symbol` objects are not serializable
1190
+ * - And much more...
1191
+ *
1192
+ * @throws UnexpectedError if the value is not serializable as JSON
1193
+ * @public exported from `@promptbook/utils`
1194
+ */
1195
+ function checkSerializableAsJson(options) {
1196
+ const { value, name, message } = options;
1197
+ if (value === undefined) {
1198
+ throw new UnexpectedError(`${name} is undefined`);
1199
+ }
1200
+ else if (value === null) {
1201
+ return;
1202
+ }
1203
+ else if (typeof value === 'boolean') {
1204
+ return;
1205
+ }
1206
+ else if (typeof value === 'number' && !isNaN(value)) {
1207
+ return;
1208
+ }
1209
+ else if (typeof value === 'string') {
1210
+ return;
1211
+ }
1212
+ else if (typeof value === 'symbol') {
1213
+ throw new UnexpectedError(`${name} is symbol`);
1214
+ }
1215
+ else if (typeof value === 'function') {
1216
+ throw new UnexpectedError(`${name} is function`);
1217
+ }
1218
+ else if (typeof value === 'object' && Array.isArray(value)) {
1219
+ for (let i = 0; i < value.length; i++) {
1220
+ checkSerializableAsJson({ name: `${name}[${i}]`, value: value[i], message });
1221
+ }
1222
+ }
1223
+ else if (typeof value === 'object') {
1224
+ if (value instanceof Date) {
1225
+ throw new UnexpectedError(spaceTrim__default["default"]((block) => `
1226
+ \`${name}\` is Date
1227
+
1228
+ Use \`string_date_iso8601\` instead
1229
+
1230
+ Additional message for \`${name}\`:
1231
+ ${block(message || '(nothing)')}
1232
+ `));
1233
+ }
1234
+ else if (value instanceof Map) {
1235
+ throw new UnexpectedError(`${name} is Map`);
1236
+ }
1237
+ else if (value instanceof Set) {
1238
+ throw new UnexpectedError(`${name} is Set`);
1239
+ }
1240
+ else if (value instanceof RegExp) {
1241
+ throw new UnexpectedError(`${name} is RegExp`);
1242
+ }
1243
+ else if (value instanceof Error) {
1244
+ throw new UnexpectedError(spaceTrim__default["default"]((block) => `
1245
+ \`${name}\` is unserialized Error
1246
+
1247
+ Use function \`serializeError\`
1248
+
1249
+ Additional message for \`${name}\`:
1250
+ ${block(message || '(nothing)')}
1251
+
1252
+ `));
1253
+ }
1254
+ else {
1255
+ for (const [subName, subValue] of Object.entries(value)) {
1256
+ if (subValue === undefined) {
1257
+ // Note: undefined in object is serializable - it is just omited
1258
+ continue;
1259
+ }
1260
+ checkSerializableAsJson({ name: `${name}.${subName}`, value: subValue, message });
1261
+ }
1262
+ try {
1263
+ JSON.stringify(value); // <- TODO: [0]
1264
+ }
1265
+ catch (error) {
1266
+ if (!(error instanceof Error)) {
1267
+ throw error;
1268
+ }
1269
+ throw new UnexpectedError(spaceTrim__default["default"]((block) => `
1270
+ \`${name}\` is not serializable
1271
+
1272
+ ${block(error.stack || error.message)}
1273
+
1274
+ Additional message for \`${name}\`:
1275
+ ${block(message || '(nothing)')}
1276
+ `));
1277
+ }
1278
+ /*
1279
+ TODO: [0] Is there some more elegant way to check circular references?
1280
+ const seen = new Set();
1281
+ const stack = [{ value }];
1282
+ while (stack.length > 0) {
1283
+ const { value } = stack.pop()!;
1284
+ if (typeof value === 'object' && value !== null) {
1285
+ if (seen.has(value)) {
1286
+ throw new UnexpectedError(`${name} has circular reference`);
1287
+ }
1288
+ seen.add(value);
1289
+ if (Array.isArray(value)) {
1290
+ stack.push(...value.map((value) => ({ value })));
1291
+ } else {
1292
+ stack.push(...Object.values(value).map((value) => ({ value })));
1293
+ }
1294
+ }
1295
+ }
1296
+ */
1297
+ return;
1298
+ }
1299
+ }
1300
+ else {
1301
+ throw new UnexpectedError(spaceTrim__default["default"]((block) => `
1302
+ \`${name}\` is unknown type
1303
+
1304
+ Additional message for \`${name}\`:
1305
+ ${block(message || '(nothing)')}
1306
+ `));
1307
+ }
1308
+ }
1309
+ /**
1310
+ * TODO: Can be return type more type-safe? like `asserts options.value is JsonValue`
1311
+ * TODO: [🧠][main] !!3 In-memory cache of same values to prevent multiple checks
1312
+ * Note: [🐠] This is how `checkSerializableAsJson` + `isSerializableAsJson` together can just retun true/false or rich error message
1313
+ */
1314
+
1315
+ /**
1316
+ * @@@
1317
+ *
1318
+ * @public exported from `@promptbook/utils`
1319
+ */
1320
+ function deepClone(objectValue) {
1321
+ return JSON.parse(JSON.stringify(objectValue));
1322
+ /*
1323
+ TODO: [🧠] Is there a better implementation?
1324
+ > const propertyNames = Object.getOwnPropertyNames(objectValue);
1325
+ > for (const propertyName of propertyNames) {
1326
+ > const value = (objectValue as really_any)[propertyName];
1327
+ > if (value && typeof value === 'object') {
1328
+ > deepClone(value);
1329
+ > }
1330
+ > }
1331
+ > return Object.assign({}, objectValue);
1332
+ */
1333
+ }
1334
+ /**
1335
+ * TODO: [🧠] Is there a way how to meaningfully test this utility
1336
+ */
1337
+
1338
+ /**
1339
+ * Utility to export a JSON object from a function
1340
+ *
1341
+ * 1) Checks if the value is serializable as JSON
1342
+ * 2) Makes a deep clone of the object
1343
+ * 2) Orders the object properties
1344
+ * 2) Deeply freezes the cloned object
1345
+ *
1346
+ * Note: This function does not mutates the given object
1347
+ *
1348
+ * @returns The same type of object as the input but read-only and re-ordered
1349
+ * @public exported from `@promptbook/utils`
1350
+ */
1351
+ function exportJson(options) {
1352
+ const { name, value, order, message } = options;
1353
+ checkSerializableAsJson({ name, value, message });
1354
+ const orderedValue =
1355
+ // TODO: Fix error "Type instantiation is excessively deep and possibly infinite."
1356
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1357
+ // @ts-ignore
1358
+ order === undefined
1359
+ ? deepClone(value)
1360
+ : orderJson({
1361
+ value: value,
1362
+ // <- Note: checkSerializableAsJson asserts that the value is serializable as JSON
1363
+ order: order,
1364
+ });
1365
+ $deepFreeze(orderedValue);
1366
+ return orderedValue;
1367
+ }
1368
+ /**
1369
+ * TODO: [🧠] Is there a way how to meaningfully test this utility
1370
+ */
1371
+
1372
+ /**
1373
+ * Order of keys in the pipeline JSON
1374
+ *
1375
+ * @public exported from `@promptbook/core`
1376
+ */
1377
+ const ORDER_OF_PIPELINE_JSON = [
1378
+ // Note: [🍙] In this order will be pipeline serialized
1379
+ 'title',
1380
+ 'pipelineUrl',
1381
+ 'bookVersion',
1382
+ 'description',
1383
+ 'formfactorName',
1384
+ 'parameters',
1385
+ 'tasks',
1386
+ 'personas',
1387
+ 'preparations',
1388
+ 'knowledgeSources',
1389
+ 'knowledgePieces',
1390
+ 'sources', // <- TODO: [🧠] Where should the `sources` be
1391
+ ];
1392
+ /**
1393
+ * Nonce which is used for replacing things in strings
1394
+ *
1395
+ * @private within the repository
1396
+ */
1397
+ const REPLACING_NONCE = 'ptbkauk42kV2dzao34faw7FudQUHYPtW';
1398
+ /**
1399
+ * @@@
1400
+ *
1401
+ * @private within the repository
1402
+ */
1403
+ const RESERVED_PARAMETER_MISSING_VALUE = 'MISSING-' + REPLACING_NONCE;
1404
+ /**
1405
+ * @@@
1406
+ *
1407
+ * @private within the repository
1408
+ */
1409
+ const RESERVED_PARAMETER_RESTRICTED = 'RESTRICTED-' + REPLACING_NONCE;
1410
+ /**
1411
+ * The names of the parameters that are reserved for special purposes
1412
+ *
1413
+ * @public exported from `@promptbook/core`
1414
+ */
1415
+ const RESERVED_PARAMETER_NAMES = exportJson({
1416
+ name: 'RESERVED_PARAMETER_NAMES',
1417
+ message: `The names of the parameters that are reserved for special purposes`,
1418
+ value: [
1419
+ 'content',
1420
+ 'context',
1421
+ 'knowledge',
1422
+ 'examples',
1423
+ 'modelName',
1424
+ 'currentDate',
1425
+ // <- TODO: list here all command names
1426
+ // <- TODO: Add more like 'date', 'modelName',...
1427
+ // <- TODO: Add [emoji] + instructions ACRY when adding new reserved parameter
1428
+ ],
1429
+ });
1430
+ /**
1431
+ * Note: [💞] Ignore a discrepancy between file name and entity name
1432
+ */
1433
+
1434
+ // <- TODO: Auto convert to type `import { ... } from 'type-fest';`
1435
+ /**
1436
+ * Tests if the value is [🚉] serializable as JSON
1437
+ *
1438
+ * - Almost all primitives are serializable BUT:
1439
+ * - `undefined` is not serializable
1440
+ * - `NaN` is not serializable
1441
+ * - Objects and arrays are serializable if all their properties are serializable
1442
+ * - Functions are not serializable
1443
+ * - Circular references are not serializable
1444
+ * - `Date` objects are not serializable
1445
+ * - `Map` and `Set` objects are not serializable
1446
+ * - `RegExp` objects are not serializable
1447
+ * - `Error` objects are not serializable
1448
+ * - `Symbol` objects are not serializable
1449
+ * - And much more...
1450
+ *
1451
+ *
1452
+ * @public exported from `@promptbook/utils`
1453
+ */
1454
+ function isSerializableAsJson(value) {
1455
+ try {
1456
+ checkSerializableAsJson({ value });
1457
+ return true;
1458
+ }
1459
+ catch (error) {
1460
+ return false;
1461
+ }
1462
+ }
1463
+ /**
1464
+ * TODO: [🧠][main] !!3 In-memory cache of same values to prevent multiple checks
1465
+ * TODO: [🧠][💺] Can be done this on type-level?
1466
+ */
1467
+
1468
+ /**
1469
+ * Stringify the PipelineJson with proper formatting
1470
+ *
1471
+ * Note: [0] It can be used for more JSON types like whole collection of pipelines, single knowledge piece, etc.
1472
+ * Note: In contrast to JSON.stringify, this function ensures that **embedding index** is on single line
1473
+ *
1474
+ * @public exported from `@promptbook/editable`
1475
+ */
1476
+ function stringifyPipelineJson(pipeline) {
1477
+ if (!isSerializableAsJson(pipeline)) {
1478
+ throw new UnexpectedError(spaceTrim__default["default"](`
1479
+ Cannot stringify the pipeline, because it is not serializable as JSON
1480
+
1481
+ There can be multiple reasons:
1482
+ 1) The pipeline contains circular references
1483
+ 2) It is not a valid PipelineJson
1484
+ `));
1485
+ }
1486
+ let pipelineJsonStringified = JSON.stringify(pipeline, null, 4);
1487
+ for (let i = 0; i < LOOP_LIMIT; i++) {
1488
+ pipelineJsonStringified = pipelineJsonStringified.replace(/(-?0\.\d+),[\n\s]+(-?0\.\d+)/gms, `$1${REPLACING_NONCE}$2`);
1489
+ }
1490
+ pipelineJsonStringified = pipelineJsonStringified.split(REPLACING_NONCE).join(', ');
1491
+ pipelineJsonStringified += '\n';
1492
+ return pipelineJsonStringified;
1493
+ }
1494
+ /**
1495
+ * TODO: [🐝] Not Working propperly @see https://promptbook.studio/examples/mixed-knowledge.book
1496
+ * TODO: [🧠][0] Maybe rename to `stringifyPipelineJson`, `stringifyIndexedJson`,...
1497
+ * TODO: [🧠] Maybe more elegant solution than replacing via regex
1498
+ * TODO: [🍙] Make some standard order of json properties
1499
+ */
1500
+
1501
+ /**
1502
+ * Removes emojis from a string and fix whitespaces
1503
+ *
1504
+ * @param text with emojis
1505
+ * @returns text without emojis
1506
+ * @public exported from `@promptbook/utils`
1507
+ */
1508
+ function removeEmojis(text) {
1509
+ // Replace emojis (and also ZWJ sequence) with hyphens
1510
+ text = text.replace(/(\p{Extended_Pictographic})\p{Modifier_Symbol}/gu, '$1');
1511
+ text = text.replace(/(\p{Extended_Pictographic})[\u{FE00}-\u{FE0F}]/gu, '$1');
1512
+ text = text.replace(/(\p{Extended_Pictographic})(\u{200D}\p{Extended_Pictographic})*/gu, '$1');
1513
+ text = text.replace(/\p{Extended_Pictographic}/gu, '');
1514
+ return text;
1515
+ }
1516
+
1517
+ /**
1518
+ * Tests if given string is valid URL.
1519
+ *
1520
+ * Note: This does not check if the file exists only if the path is valid
1521
+ * @public exported from `@promptbook/utils`
1522
+ */
1523
+ function isValidFilePath(filename) {
1524
+ if (typeof filename !== 'string') {
1525
+ return false;
1526
+ }
1527
+ if (filename.split('\n').length > 1) {
1528
+ return false;
1529
+ }
1530
+ if (filename.split(' ').length >
1531
+ 5 /* <- TODO: [🧠][🈷] Make some better non-arbitrary way how to distinct filenames from informational texts */) {
1532
+ return false;
1533
+ }
1534
+ const filenameSlashes = filename.split('\\').join('/');
1535
+ // Absolute Unix path: /hello.txt
1536
+ if (/^(\/)/i.test(filenameSlashes)) {
1537
+ // console.log(filename, 'Absolute Unix path: /hello.txt');
1538
+ return true;
1539
+ }
1540
+ // Absolute Windows path: /hello.txt
1541
+ if (/^([A-Z]{1,2}:\/?)\//i.test(filenameSlashes)) {
1542
+ // console.log(filename, 'Absolute Windows path: /hello.txt');
1543
+ return true;
1544
+ }
1545
+ // Relative path: ./hello.txt
1546
+ if (/^(\.\.?\/)+/i.test(filenameSlashes)) {
1547
+ // console.log(filename, 'Relative path: ./hello.txt');
1548
+ return true;
1549
+ }
1550
+ // Allow paths like foo/hello
1551
+ if (/^[^/]+\/[^/]+/i.test(filenameSlashes)) {
1552
+ // console.log(filename, 'Allow paths like foo/hello');
1553
+ return true;
1554
+ }
1555
+ // Allow paths like hello.book
1556
+ if (/^[^/]+\.[^/]+$/i.test(filenameSlashes)) {
1557
+ // console.log(filename, 'Allow paths like hello.book');
1558
+ return true;
1559
+ }
1560
+ return false;
1561
+ }
1562
+ /**
1563
+ * TODO: [🍏] Implement for MacOs
1564
+ */
1565
+
1566
+ /**
1567
+ * Tests if given string is valid URL.
1568
+ *
1569
+ * Note: Dataurl are considered perfectly valid.
1570
+ * Note: There are two simmilar functions:
1571
+ * - `isValidUrl` which tests any URL
1572
+ * - `isValidPipelineUrl` *(this one)* which tests just promptbook URL
1573
+ *
1574
+ * @public exported from `@promptbook/utils`
1575
+ */
1576
+ function isValidUrl(url) {
1577
+ if (typeof url !== 'string') {
1578
+ return false;
1579
+ }
1580
+ try {
1581
+ if (url.startsWith('blob:')) {
1582
+ url = url.replace(/^blob:/, '');
1583
+ }
1584
+ const urlObject = new URL(url /* because fail is handled */);
1585
+ if (!['http:', 'https:', 'data:'].includes(urlObject.protocol)) {
1586
+ return false;
1587
+ }
1588
+ return true;
1589
+ }
1590
+ catch (error) {
1591
+ return false;
1592
+ }
1593
+ }
1594
+
1100
1595
  const defaultDiacriticsRemovalMap = [
1101
1596
  {
1102
1597
  base: 'A',
@@ -1509,690 +2004,792 @@
1509
2004
  */
1510
2005
 
1511
2006
  /**
1512
- * Generates random token
2007
+ * This error indicates problems parsing the format value
1513
2008
  *
1514
- * Note: This function is cryptographically secure (it uses crypto.randomBytes internally)
2009
+ * For example, when the format value is not a valid JSON or CSV
2010
+ * This is not thrown directly but in extended classes
1515
2011
  *
1516
- * @private internal helper function
1517
- * @returns secure random token
2012
+ * @public exported from `@promptbook/core`
1518
2013
  */
1519
- function $randomToken(randomness) {
1520
- return crypto.randomBytes(randomness).toString('hex');
2014
+ class AbstractFormatError extends Error {
2015
+ // Note: To allow instanceof do not put here error `name`
2016
+ // public readonly name = 'AbstractFormatError';
2017
+ constructor(message) {
2018
+ super(message);
2019
+ Object.setPrototypeOf(this, AbstractFormatError.prototype);
2020
+ }
1521
2021
  }
2022
+
1522
2023
  /**
1523
- * TODO: Maybe use nanoid instead https://github.com/ai/nanoid
2024
+ * This error indicates problem with parsing of CSV
2025
+ *
2026
+ * @public exported from `@promptbook/core`
1524
2027
  */
2028
+ class CsvFormatError extends AbstractFormatError {
2029
+ constructor(message) {
2030
+ super(message);
2031
+ this.name = 'CsvFormatError';
2032
+ Object.setPrototypeOf(this, CsvFormatError.prototype);
2033
+ }
2034
+ }
1525
2035
 
1526
2036
  /**
1527
- * This error indicates errors during the execution of the pipeline
2037
+ * AuthenticationError is thrown from login function which is dependency of remote server
1528
2038
  *
1529
2039
  * @public exported from `@promptbook/core`
1530
2040
  */
1531
- class PipelineExecutionError extends Error {
2041
+ class AuthenticationError extends Error {
1532
2042
  constructor(message) {
1533
- // Added id parameter
1534
2043
  super(message);
1535
- this.name = 'PipelineExecutionError';
1536
- // TODO: [🐙] DRY - Maybe $randomId
1537
- this.id = `error-${$randomToken(8 /* <- TODO: To global config + Use Base58 to avoid simmilar char conflicts */)}`;
1538
- Object.setPrototypeOf(this, PipelineExecutionError.prototype);
2044
+ this.name = 'AuthenticationError';
2045
+ Object.setPrototypeOf(this, AuthenticationError.prototype);
1539
2046
  }
1540
2047
  }
2048
+
1541
2049
  /**
1542
- * TODO: !!!!!! Add id to all errors
2050
+ * This error indicates that the pipeline collection cannot be propperly loaded
2051
+ *
2052
+ * @public exported from `@promptbook/core`
1543
2053
  */
2054
+ class CollectionError extends Error {
2055
+ constructor(message) {
2056
+ super(message);
2057
+ this.name = 'CollectionError';
2058
+ Object.setPrototypeOf(this, CollectionError.prototype);
2059
+ }
2060
+ }
1544
2061
 
1545
2062
  /**
1546
- * Stores data in memory (HEAP)
2063
+ * This error occurs when some expectation is not met in the execution of the pipeline
1547
2064
  *
1548
2065
  * @public exported from `@promptbook/core`
2066
+ * Note: Do not throw this error, its reserved for `checkExpectations` and `createPipelineExecutor` and public ONLY to be serializable through remote server
2067
+ * Note: Always thrown in `checkExpectations` and catched in `createPipelineExecutor` and rethrown as `PipelineExecutionError`
2068
+ * Note: This is a kindof subtype of PipelineExecutionError
1549
2069
  */
1550
- class MemoryStorage {
1551
- constructor() {
1552
- this.storage = {};
1553
- }
1554
- /**
1555
- * Returns the number of key/value pairs currently present in the list associated with the object.
1556
- */
1557
- get length() {
1558
- return Object.keys(this.storage).length;
1559
- }
1560
- /**
1561
- * Empties the list associated with the object of all key/value pairs, if there are any.
1562
- */
1563
- clear() {
1564
- this.storage = {};
1565
- }
1566
- /**
1567
- * Returns the current value associated with the given key, or null if the given key does not exist in the list associated with the object.
1568
- */
1569
- getItem(key) {
1570
- return this.storage[key] || null;
1571
- }
1572
- /**
1573
- * Returns the name of the nth key in the list, or null if n is greater than or equal to the number of key/value pairs in the object.
1574
- */
1575
- key(index) {
1576
- return Object.keys(this.storage)[index] || null;
1577
- }
1578
- /**
1579
- * Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.
1580
- */
1581
- setItem(key, value) {
1582
- this.storage[key] = value;
1583
- }
1584
- /**
1585
- * Removes the key/value pair with the given key from the list associated with the object, if a key/value pair with the given key exists.
1586
- */
1587
- removeItem(key) {
1588
- delete this.storage[key];
2070
+ class ExpectError extends Error {
2071
+ constructor(message) {
2072
+ super(message);
2073
+ this.name = 'ExpectError';
2074
+ Object.setPrototypeOf(this, ExpectError.prototype);
1589
2075
  }
1590
2076
  }
1591
2077
 
1592
2078
  /**
1593
- * Simple wrapper `new Date().toISOString()`
1594
- *
1595
- * Note: `$` is used to indicate that this function is not a pure function - it is not deterministic because it depends on the current time
2079
+ * This error indicates that the promptbook can not retrieve knowledge from external sources
1596
2080
  *
1597
- * @returns string_date branded type
1598
- * @public exported from `@promptbook/utils`
2081
+ * @public exported from `@promptbook/core`
1599
2082
  */
1600
- function $getCurrentDate() {
1601
- return new Date().toISOString();
2083
+ class KnowledgeScrapeError extends Error {
2084
+ constructor(message) {
2085
+ super(message);
2086
+ this.name = 'KnowledgeScrapeError';
2087
+ Object.setPrototypeOf(this, KnowledgeScrapeError.prototype);
2088
+ }
1602
2089
  }
1603
2090
 
1604
2091
  /**
1605
- * Intercepts LLM tools and counts total usage of the tools
1606
- *
1607
- * Note: It can take extended `LlmExecutionTools` and cache the
2092
+ * This error type indicates that some limit was reached
1608
2093
  *
1609
- * @param llmTools LLM tools to be intercepted with usage counting, it can contain extra methods like `totalUsage`
1610
- * @returns LLM tools with same functionality with added total cost counting
1611
2094
  * @public exported from `@promptbook/core`
1612
2095
  */
1613
- function cacheLlmTools(llmTools, options = {}) {
1614
- const { storage = new MemoryStorage(), isCacheReloaded = false } = options;
1615
- const proxyTools = {
1616
- ...llmTools,
1617
- // <- Note: [🥫]
1618
- get title() {
1619
- // TODO: [🧠] Maybe put here some suffix
1620
- return llmTools.title;
1621
- },
1622
- get description() {
1623
- // TODO: [🧠] Maybe put here some suffix
1624
- return llmTools.description;
1625
- },
1626
- listModels() {
1627
- // TODO: [🧠] Should be model listing also cached?
1628
- return /* not await */ llmTools.listModels();
1629
- },
1630
- };
1631
- const callCommonModel = async (prompt) => {
1632
- const { parameters, content, modelRequirements } = prompt;
1633
- // <- Note: These are relevant things from the prompt that the cache key should depend on.
1634
- const key = titleToName(prompt.title.substring(0, MAX_FILENAME_LENGTH - 10) +
1635
- '-' +
1636
- sha256__default["default"](hexEncoder__default["default"].parse(JSON.stringify({ parameters, content, modelRequirements }))).toString( /* hex */));
1637
- const cacheItem = !isCacheReloaded ? await storage.getItem(key) : null;
1638
- if (cacheItem) {
1639
- return cacheItem.promptResult;
1640
- }
1641
- let promptResult;
1642
- variant: switch (prompt.modelRequirements.modelVariant) {
1643
- case 'CHAT':
1644
- promptResult = await llmTools.callChatModel(prompt);
1645
- break variant;
1646
- case 'COMPLETION':
1647
- promptResult = await llmTools.callCompletionModel(prompt);
1648
- break variant;
1649
- case 'EMBEDDING':
1650
- promptResult = await llmTools.callEmbeddingModel(prompt);
1651
- break variant;
1652
- // <- case [🤖]:
1653
- default:
1654
- throw new PipelineExecutionError(`Unknown model variant "${prompt.modelRequirements.modelVariant}"`);
1655
- }
1656
- // TODO: [🧠] !!5 How to do timing in mixed cache / non-cache situation
1657
- // promptResult.timing: FromtoItems
1658
- await storage.setItem(key, {
1659
- date: $getCurrentDate(),
1660
- promptbookVersion: PROMPTBOOK_ENGINE_VERSION,
1661
- prompt,
1662
- promptResult,
1663
- });
1664
- return promptResult;
1665
- };
1666
- if (llmTools.callChatModel !== undefined) {
1667
- proxyTools.callChatModel = async (prompt) => {
1668
- return /* not await */ callCommonModel(prompt);
1669
- };
1670
- }
1671
- if (llmTools.callCompletionModel !== undefined) {
1672
- proxyTools.callCompletionModel = async (prompt) => {
1673
- return /* not await */ callCommonModel(prompt);
1674
- };
1675
- }
1676
- if (llmTools.callEmbeddingModel !== undefined) {
1677
- proxyTools.callEmbeddingModel = async (prompt) => {
1678
- return /* not await */ callCommonModel(prompt);
1679
- };
2096
+ class LimitReachedError extends Error {
2097
+ constructor(message) {
2098
+ super(message);
2099
+ this.name = 'LimitReachedError';
2100
+ Object.setPrototypeOf(this, LimitReachedError.prototype);
1680
2101
  }
1681
- // <- Note: [🤖]
1682
- return proxyTools;
1683
2102
  }
2103
+
1684
2104
  /**
1685
- * TODO: [🧠][💸] Maybe make some common abstraction `interceptLlmTools` and use here (or use javascript Proxy?)
1686
- * TODO: [🧠] Is there some meaningfull way how to test this util
1687
- * TODO: [👷‍♂️] @@@ Manual about construction of llmTools
1688
- * @@@ write discussion about this and storages
1689
- * @@@ write how to combine multiple interceptors
2105
+ * This error type indicates that some tools are missing for pipeline execution or preparation
2106
+ *
2107
+ * @public exported from `@promptbook/core`
1690
2108
  */
2109
+ class MissingToolsError extends Error {
2110
+ constructor(message) {
2111
+ super(spaceTrim.spaceTrim((block) => `
2112
+ ${block(message)}
2113
+
2114
+ Note: You have probbably forgot to provide some tools for pipeline execution or preparation
2115
+
2116
+ `));
2117
+ this.name = 'MissingToolsError';
2118
+ Object.setPrototypeOf(this, MissingToolsError.prototype);
2119
+ }
2120
+ }
1691
2121
 
1692
2122
  /**
1693
- * Represents the usage with no resources consumed
2123
+ * This error indicates that promptbook not found in the collection
1694
2124
  *
1695
2125
  * @public exported from `@promptbook/core`
1696
2126
  */
1697
- const ZERO_USAGE = $deepFreeze({
1698
- price: { value: 0 },
1699
- input: {
1700
- tokensCount: { value: 0 },
1701
- charactersCount: { value: 0 },
1702
- wordsCount: { value: 0 },
1703
- sentencesCount: { value: 0 },
1704
- linesCount: { value: 0 },
1705
- paragraphsCount: { value: 0 },
1706
- pagesCount: { value: 0 },
1707
- },
1708
- output: {
1709
- tokensCount: { value: 0 },
1710
- charactersCount: { value: 0 },
1711
- wordsCount: { value: 0 },
1712
- sentencesCount: { value: 0 },
1713
- linesCount: { value: 0 },
1714
- paragraphsCount: { value: 0 },
1715
- pagesCount: { value: 0 },
1716
- },
1717
- });
2127
+ class NotFoundError extends Error {
2128
+ constructor(message) {
2129
+ super(message);
2130
+ this.name = 'NotFoundError';
2131
+ Object.setPrototypeOf(this, NotFoundError.prototype);
2132
+ }
2133
+ }
2134
+
1718
2135
  /**
1719
- * Represents the usage with unknown resources consumed
2136
+ * This error indicates that the promptbook in a markdown format cannot be parsed into a valid promptbook object
1720
2137
  *
1721
2138
  * @public exported from `@promptbook/core`
1722
2139
  */
1723
- const UNCERTAIN_USAGE = $deepFreeze({
1724
- price: { value: 0, isUncertain: true },
1725
- input: {
1726
- tokensCount: { value: 0, isUncertain: true },
1727
- charactersCount: { value: 0, isUncertain: true },
1728
- wordsCount: { value: 0, isUncertain: true },
1729
- sentencesCount: { value: 0, isUncertain: true },
1730
- linesCount: { value: 0, isUncertain: true },
1731
- paragraphsCount: { value: 0, isUncertain: true },
1732
- pagesCount: { value: 0, isUncertain: true },
1733
- },
1734
- output: {
1735
- tokensCount: { value: 0, isUncertain: true },
1736
- charactersCount: { value: 0, isUncertain: true },
1737
- wordsCount: { value: 0, isUncertain: true },
1738
- sentencesCount: { value: 0, isUncertain: true },
1739
- linesCount: { value: 0, isUncertain: true },
1740
- paragraphsCount: { value: 0, isUncertain: true },
1741
- pagesCount: { value: 0, isUncertain: true },
1742
- },
1743
- });
2140
+ class ParseError extends Error {
2141
+ constructor(message) {
2142
+ super(message);
2143
+ this.name = 'ParseError';
2144
+ Object.setPrototypeOf(this, ParseError.prototype);
2145
+ }
2146
+ }
1744
2147
  /**
1745
- * Note: [💞] Ignore a discrepancy between file name and entity name
2148
+ * TODO: Maybe split `ParseError` and `ApplyError`
1746
2149
  */
1747
2150
 
1748
2151
  /**
1749
- * Function `addUsage` will add multiple usages into one
2152
+ * Generates random token
1750
2153
  *
1751
- * Note: If you provide 0 values, it returns ZERO_USAGE
2154
+ * Note: This function is cryptographically secure (it uses crypto.randomBytes internally)
1752
2155
  *
1753
- * @public exported from `@promptbook/core`
2156
+ * @private internal helper function
2157
+ * @returns secure random token
1754
2158
  */
1755
- function addUsage(...usageItems) {
1756
- return usageItems.reduce((acc, item) => {
1757
- var _a;
1758
- acc.price.value += ((_a = item.price) === null || _a === void 0 ? void 0 : _a.value) || 0;
1759
- for (const key of Object.keys(acc.input)) {
1760
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1761
- //@ts-ignore
1762
- if (item.input[key]) {
1763
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1764
- //@ts-ignore
1765
- acc.input[key].value += item.input[key].value || 0;
1766
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1767
- //@ts-ignore
1768
- if (item.input[key].isUncertain) {
1769
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1770
- //@ts-ignore
1771
- acc.input[key].isUncertain = true;
1772
- }
1773
- }
1774
- }
1775
- for (const key of Object.keys(acc.output)) {
1776
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1777
- //@ts-ignore
1778
- if (item.output[key]) {
1779
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1780
- //@ts-ignore
1781
- acc.output[key].value += item.output[key].value || 0;
1782
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1783
- //@ts-ignore
1784
- if (item.output[key].isUncertain) {
1785
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1786
- //@ts-ignore
1787
- acc.output[key].isUncertain = true;
1788
- }
1789
- }
1790
- }
1791
- return acc;
1792
- }, deepClone(ZERO_USAGE));
2159
+ function $randomToken(randomness) {
2160
+ return crypto.randomBytes(randomness).toString('hex');
1793
2161
  }
2162
+ /**
2163
+ * TODO: Maybe use nanoid instead https://github.com/ai/nanoid
2164
+ */
1794
2165
 
1795
2166
  /**
1796
- * Intercepts LLM tools and counts total usage of the tools
2167
+ * This error indicates errors during the execution of the pipeline
1797
2168
  *
1798
- * @param llmTools LLM tools to be intercepted with usage counting
1799
- * @returns LLM tools with same functionality with added total cost counting
1800
2169
  * @public exported from `@promptbook/core`
1801
2170
  */
1802
- function countTotalUsage(llmTools) {
1803
- let totalUsage = ZERO_USAGE;
1804
- const proxyTools = {
1805
- get title() {
1806
- // TODO: [🧠] Maybe put here some suffix
1807
- return llmTools.title;
1808
- },
1809
- get description() {
1810
- // TODO: [🧠] Maybe put here some suffix
1811
- return llmTools.description;
1812
- },
1813
- async checkConfiguration() {
1814
- return /* not await */ llmTools.checkConfiguration();
1815
- },
1816
- listModels() {
1817
- return /* not await */ llmTools.listModels();
1818
- },
1819
- getTotalUsage() {
1820
- // <- Note: [🥫] Not using getter `get totalUsage` but `getTotalUsage` to allow this object to be proxied
1821
- return totalUsage;
1822
- },
1823
- };
1824
- if (llmTools.callChatModel !== undefined) {
1825
- proxyTools.callChatModel = async (prompt) => {
1826
- // console.info('[🚕] callChatModel through countTotalUsage');
1827
- const promptResult = await llmTools.callChatModel(prompt);
1828
- totalUsage = addUsage(totalUsage, promptResult.usage);
1829
- return promptResult;
1830
- };
1831
- }
1832
- if (llmTools.callCompletionModel !== undefined) {
1833
- proxyTools.callCompletionModel = async (prompt) => {
1834
- // console.info('[🚕] callCompletionModel through countTotalUsage');
1835
- const promptResult = await llmTools.callCompletionModel(prompt);
1836
- totalUsage = addUsage(totalUsage, promptResult.usage);
1837
- return promptResult;
1838
- };
1839
- }
1840
- if (llmTools.callEmbeddingModel !== undefined) {
1841
- proxyTools.callEmbeddingModel = async (prompt) => {
1842
- // console.info('[🚕] callEmbeddingModel through countTotalUsage');
1843
- const promptResult = await llmTools.callEmbeddingModel(prompt);
1844
- totalUsage = addUsage(totalUsage, promptResult.usage);
1845
- return promptResult;
1846
- };
2171
+ class PipelineExecutionError extends Error {
2172
+ constructor(message) {
2173
+ // Added id parameter
2174
+ super(message);
2175
+ this.name = 'PipelineExecutionError';
2176
+ // TODO: [🐙] DRY - Maybe $randomId
2177
+ this.id = `error-${$randomToken(8 /* <- TODO: To global config + Use Base58 to avoid simmilar char conflicts */)}`;
2178
+ Object.setPrototypeOf(this, PipelineExecutionError.prototype);
1847
2179
  }
1848
- // <- Note: [🤖]
1849
- return proxyTools;
1850
2180
  }
1851
2181
  /**
1852
- * TODO: [🧠][💸] Maybe make some common abstraction `interceptLlmTools` and use here (or use javascript Proxy?)
1853
- * TODO: [🧠] Is there some meaningfull way how to test this util
1854
- * TODO: [🧠][🌯] Maybe a way how to hide ability to `get totalUsage`
1855
- * > const [llmToolsWithUsage,getUsage] = countTotalUsage(llmTools);
1856
- * TODO: [👷‍♂️] @@@ Manual about construction of llmTools
2182
+ * TODO: !!!!!! Add id to all errors
1857
2183
  */
1858
2184
 
1859
2185
  /**
1860
- * This error type indicates that some part of the code is not implemented yet
2186
+ * This error indicates that the promptbook object has valid syntax (=can be parsed) but contains logical errors (like circular dependencies)
1861
2187
  *
1862
2188
  * @public exported from `@promptbook/core`
1863
2189
  */
1864
- class NotYetImplementedError extends Error {
2190
+ class PipelineLogicError extends Error {
1865
2191
  constructor(message) {
1866
- super(spaceTrim.spaceTrim((block) => `
1867
- ${block(message)}
2192
+ super(message);
2193
+ this.name = 'PipelineLogicError';
2194
+ Object.setPrototypeOf(this, PipelineLogicError.prototype);
2195
+ }
2196
+ }
1868
2197
 
1869
- Note: This feature is not implemented yet but it will be soon.
2198
+ /**
2199
+ * This error indicates errors in referencing promptbooks between each other
2200
+ *
2201
+ * @public exported from `@promptbook/core`
2202
+ */
2203
+ class PipelineUrlError extends Error {
2204
+ constructor(message) {
2205
+ super(message);
2206
+ this.name = 'PipelineUrlError';
2207
+ Object.setPrototypeOf(this, PipelineUrlError.prototype);
2208
+ }
2209
+ }
1870
2210
 
1871
- If you want speed up the implementation or just read more, look here:
1872
- https://github.com/webgptorg/promptbook
2211
+ /**
2212
+ * Error thrown when a fetch request fails
2213
+ *
2214
+ * @public exported from `@promptbook/core`
2215
+ */
2216
+ class PromptbookFetchError extends Error {
2217
+ constructor(message) {
2218
+ super(message);
2219
+ this.name = 'PromptbookFetchError';
2220
+ Object.setPrototypeOf(this, PromptbookFetchError.prototype);
2221
+ }
2222
+ }
1873
2223
 
1874
- Or contact us on pavol@ptbk.io
2224
+ /**
2225
+ * Index of all custom errors
2226
+ *
2227
+ * @public exported from `@promptbook/core`
2228
+ */
2229
+ const PROMPTBOOK_ERRORS = {
2230
+ AbstractFormatError,
2231
+ CsvFormatError,
2232
+ CollectionError,
2233
+ EnvironmentMismatchError,
2234
+ ExpectError,
2235
+ KnowledgeScrapeError,
2236
+ LimitReachedError,
2237
+ MissingToolsError,
2238
+ NotFoundError,
2239
+ NotYetImplementedError,
2240
+ ParseError,
2241
+ PipelineExecutionError,
2242
+ PipelineLogicError,
2243
+ PipelineUrlError,
2244
+ UnexpectedError,
2245
+ // TODO: [🪑]> VersionMismatchError,
2246
+ };
2247
+ /**
2248
+ * Index of all javascript errors
2249
+ *
2250
+ * @private for internal usage
2251
+ */
2252
+ const COMMON_JAVASCRIPT_ERRORS = {
2253
+ Error,
2254
+ EvalError,
2255
+ RangeError,
2256
+ ReferenceError,
2257
+ SyntaxError,
2258
+ TypeError,
2259
+ URIError,
2260
+ AggregateError,
2261
+ AuthenticationError,
2262
+ PromptbookFetchError,
2263
+ /*
2264
+ Note: Not widely supported
2265
+ > InternalError,
2266
+ > ModuleError,
2267
+ > HeapError,
2268
+ > WebAssemblyCompileError,
2269
+ > WebAssemblyRuntimeError,
2270
+ */
2271
+ };
2272
+ /**
2273
+ * Index of all errors
2274
+ *
2275
+ * @private for internal usage
2276
+ */
2277
+ const ALL_ERRORS = {
2278
+ ...PROMPTBOOK_ERRORS,
2279
+ ...COMMON_JAVASCRIPT_ERRORS,
2280
+ };
2281
+ /**
2282
+ * Note: [💞] Ignore a discrepancy between file name and entity name
2283
+ */
2284
+
2285
+ /**
2286
+ * Deserializes the error object
2287
+ *
2288
+ * @public exported from `@promptbook/utils`
2289
+ */
2290
+ function deserializeError(error) {
2291
+ const { name, stack, id } = error; // Added id
2292
+ let { message } = error;
2293
+ let ErrorClass = ALL_ERRORS[error.name];
2294
+ if (ErrorClass === undefined) {
2295
+ ErrorClass = Error;
2296
+ message = `${name}: ${message}`;
2297
+ }
2298
+ if (stack !== undefined && stack !== '') {
2299
+ message = spaceTrim__default["default"]((block) => `
2300
+ ${block(message)}
1875
2301
 
1876
- `));
1877
- this.name = 'NotYetImplementedError';
1878
- Object.setPrototypeOf(this, NotYetImplementedError.prototype);
2302
+ Original stack trace:
2303
+ ${block(stack || '')}
2304
+ `);
1879
2305
  }
2306
+ const deserializedError = new ErrorClass(message);
2307
+ deserializedError.id = id; // Assign id to the error object
2308
+ return deserializedError;
1880
2309
  }
1881
2310
 
1882
2311
  /**
1883
- * @@@
2312
+ * Creates a connection to the remote proxy server.
1884
2313
  *
1885
- * Note: `$` is used to indicate that this function is not a pure function - it access global scope
2314
+ * Note: This function creates a connection to the remote server and returns a socket but responsibility of closing the connection is on the caller
1886
2315
  *
1887
- * @private internal function of `$Register`
2316
+ * @private internal utility function
1888
2317
  */
1889
- function $getGlobalScope() {
1890
- return Function('return this')();
2318
+ async function createRemoteClient(options) {
2319
+ const { remoteServerUrl } = options;
2320
+ let path = new URL(remoteServerUrl).pathname;
2321
+ if (path.endsWith('/')) {
2322
+ path = path.slice(0, -1);
2323
+ }
2324
+ path = `${path}/socket.io`;
2325
+ return new Promise((resolve, reject) => {
2326
+ const socket = socket_ioClient.io(remoteServerUrl, {
2327
+ retries: CONNECTION_RETRIES_LIMIT,
2328
+ timeout: CONNECTION_TIMEOUT_MS,
2329
+ path,
2330
+ transports: [/*'websocket', <- TODO: [🌬] Make websocket transport work */ 'polling'],
2331
+ });
2332
+ // console.log('Connecting to', this.options.remoteServerUrl.href, { socket });
2333
+ socket.on('connect', () => {
2334
+ resolve(socket);
2335
+ });
2336
+ // TODO: [💩] Better timeout handling
2337
+ setTimeout(() => {
2338
+ reject(new Error(`Timeout while connecting to ${remoteServerUrl}`));
2339
+ }, CONNECTION_TIMEOUT_MS);
2340
+ });
1891
2341
  }
1892
2342
 
1893
2343
  /**
1894
- * @@@
2344
+ * Remote server is a proxy server that uses its execution tools internally and exposes the executor interface externally.
1895
2345
  *
1896
- * @param text @@@
1897
- * @returns @@@
1898
- * @example 'HELLO_WORLD'
1899
- * @example 'I_LOVE_PROMPTBOOK'
1900
- * @public exported from `@promptbook/utils`
2346
+ * You can simply use `RemoteExecutionTools` on client-side javascript and connect to your remote server.
2347
+ * This is useful to make all logic on browser side but not expose your API keys or no need to use customer's GPU.
2348
+ *
2349
+ * @see https://github.com/webgptorg/promptbook#remote-server
2350
+ * @public exported from `@promptbook/remote-client`
1901
2351
  */
1902
- function normalizeTo_SCREAMING_CASE(text) {
1903
- let charType;
1904
- let lastCharType = 'OTHER';
1905
- let normalizedName = '';
1906
- for (const char of text) {
1907
- let normalizedChar;
1908
- if (/^[a-z]$/.test(char)) {
1909
- charType = 'LOWERCASE';
1910
- normalizedChar = char.toUpperCase();
1911
- }
1912
- else if (/^[A-Z]$/.test(char)) {
1913
- charType = 'UPPERCASE';
1914
- normalizedChar = char;
1915
- }
1916
- else if (/^[0-9]$/.test(char)) {
1917
- charType = 'NUMBER';
1918
- normalizedChar = char;
2352
+ class RemoteLlmExecutionTools {
2353
+ /* <- TODO: [🍚] `, Destroyable` */
2354
+ constructor(options) {
2355
+ this.options = options;
2356
+ }
2357
+ get title() {
2358
+ // TODO: [🧠] Maybe fetch title+description from the remote server (as well as if model methods are defined)
2359
+ return 'Remote server';
2360
+ }
2361
+ get description() {
2362
+ return 'Use all models by your remote server';
2363
+ }
2364
+ /**
2365
+ * Check the configuration of all execution tools
2366
+ */
2367
+ async checkConfiguration() {
2368
+ const socket = await createRemoteClient(this.options);
2369
+ socket.disconnect();
2370
+ // TODO: [main] !!3 Check version of the remote server and compatibility
2371
+ // TODO: [🎍] Send checkConfiguration
2372
+ }
2373
+ /**
2374
+ * List all available models that can be used
2375
+ */
2376
+ async listModels() {
2377
+ // TODO: [👒] Listing models (and checking configuration) probbably should go through REST API not Socket.io
2378
+ const socket = await createRemoteClient(this.options);
2379
+ socket.emit('listModels-request', {
2380
+ identification: this.options.identification,
2381
+ } /* <- Note: [🤛] */);
2382
+ const promptResult = await new Promise((resolve, reject) => {
2383
+ socket.on('listModels-response', (response) => {
2384
+ resolve(response.models);
2385
+ socket.disconnect();
2386
+ });
2387
+ socket.on('error', (error) => {
2388
+ reject(deserializeError(error));
2389
+ socket.disconnect();
2390
+ });
2391
+ });
2392
+ socket.disconnect();
2393
+ return promptResult;
2394
+ }
2395
+ /**
2396
+ * Calls remote proxy server to use a chat model
2397
+ */
2398
+ callChatModel(prompt) {
2399
+ if (this.options.isVerbose) {
2400
+ console.info(`🖋 Remote callChatModel call`);
1919
2401
  }
1920
- else {
1921
- charType = 'OTHER';
1922
- normalizedChar = '_';
2402
+ return /* not await */ this.callCommonModel(prompt);
2403
+ }
2404
+ /**
2405
+ * Calls remote proxy server to use a completion model
2406
+ */
2407
+ callCompletionModel(prompt) {
2408
+ if (this.options.isVerbose) {
2409
+ console.info(`💬 Remote callCompletionModel call`);
1923
2410
  }
1924
- if (charType !== lastCharType &&
1925
- !(lastCharType === 'UPPERCASE' && charType === 'LOWERCASE') &&
1926
- !(lastCharType === 'NUMBER') &&
1927
- !(charType === 'NUMBER')) {
1928
- normalizedName += '_';
2411
+ return /* not await */ this.callCommonModel(prompt);
2412
+ }
2413
+ /**
2414
+ * Calls remote proxy server to use a embedding model
2415
+ */
2416
+ callEmbeddingModel(prompt) {
2417
+ if (this.options.isVerbose) {
2418
+ console.info(`💬 Remote callEmbeddingModel call`);
1929
2419
  }
1930
- normalizedName += normalizedChar;
1931
- lastCharType = charType;
2420
+ return /* not await */ this.callCommonModel(prompt);
2421
+ }
2422
+ // <- Note: [🤖] callXxxModel
2423
+ /**
2424
+ * Calls remote proxy server to use both completion or chat model
2425
+ */
2426
+ async callCommonModel(prompt) {
2427
+ const socket = await createRemoteClient(this.options);
2428
+ socket.emit('prompt-request', {
2429
+ identification: this.options.identification,
2430
+ prompt,
2431
+ } /* <- Note: [🤛] */);
2432
+ const promptResult = await new Promise((resolve, reject) => {
2433
+ socket.on('prompt-response', (response) => {
2434
+ resolve(response.promptResult);
2435
+ socket.disconnect();
2436
+ });
2437
+ socket.on('error', (error) => {
2438
+ reject(deserializeError(error));
2439
+ socket.disconnect();
2440
+ });
2441
+ });
2442
+ socket.disconnect();
2443
+ return promptResult;
1932
2444
  }
1933
- normalizedName = normalizedName.replace(/_+/g, '_');
1934
- normalizedName = normalizedName.replace(/_?\/_?/g, '/');
1935
- normalizedName = normalizedName.replace(/^_/, '');
1936
- normalizedName = normalizedName.replace(/_$/, '');
1937
- return normalizedName;
1938
2445
  }
1939
2446
  /**
1940
- * TODO: Tests
1941
- * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: 'Moje tabule' })).toEqual('/VtG7sR9rRJqwNEdM2/Moje tabule');
1942
- * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: 'ěščřžžýáíúů' })).toEqual('/VtG7sR9rRJqwNEdM2/escrzyaieuu');
1943
- * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: ' ahoj ' })).toEqual('/VtG7sR9rRJqwNEdM2/ahoj');
1944
- * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: ' ahoj_ahojAhoj ahoj ' })).toEqual('/VtG7sR9rRJqwNEdM2/ahoj-ahoj-ahoj-ahoj');
1945
- * TODO: [🌺] Use some intermediate util splitWords
1946
- */
1947
-
1948
- /**
1949
- * @@@
1950
- *
1951
- * @param text @@@
1952
- * @returns @@@
1953
- * @example 'hello_world'
1954
- * @example 'i_love_promptbook'
1955
- * @public exported from `@promptbook/utils`
2447
+ * TODO: Maybe use `$exportJson`
2448
+ * TODO: [🧠][🛍] Maybe not `isAnonymous: boolean` BUT `mode: 'ANONYMOUS'|'COLLECTION'`
2449
+ * TODO: [🍓] Allow to list compatible models with each variant
2450
+ * TODO: [🗯] RemoteLlmExecutionTools should extend Destroyable and implement IDestroyable
2451
+ * TODO: [🧠][🌰] Allow to pass `title` for tracking purposes
2452
+ * TODO: [🧠] Maybe remove `@promptbook/remote-client` and just use `@promptbook/core`
1956
2453
  */
1957
- function normalizeTo_snake_case(text) {
1958
- return normalizeTo_SCREAMING_CASE(text).toLowerCase();
1959
- }
1960
2454
 
1961
2455
  /**
1962
- * Register is @@@
1963
- *
1964
- * Note: `$` is used to indicate that this function is not a pure function - it accesses and adds variables in global scope.
2456
+ * Stores data in memory (HEAP)
1965
2457
  *
1966
- * @private internal utility, exported are only signleton instances of this class
2458
+ * @public exported from `@promptbook/core`
1967
2459
  */
1968
- class $Register {
1969
- constructor(registerName) {
1970
- this.registerName = registerName;
1971
- const storageName = `_promptbook_${normalizeTo_snake_case(registerName)}`;
1972
- const globalScope = $getGlobalScope();
1973
- if (globalScope[storageName] === undefined) {
1974
- globalScope[storageName] = [];
1975
- }
1976
- else if (!Array.isArray(globalScope[storageName])) {
1977
- throw new UnexpectedError(`Expected (global) ${storageName} to be an array, but got ${typeof globalScope[storageName]}`);
1978
- }
1979
- this.storage = globalScope[storageName];
2460
+ class MemoryStorage {
2461
+ constructor() {
2462
+ this.storage = {};
1980
2463
  }
1981
- list() {
1982
- // <- TODO: ReadonlyDeep<ReadonlyArray<TRegistered>>
1983
- return this.storage;
2464
+ /**
2465
+ * Returns the number of key/value pairs currently present in the list associated with the object.
2466
+ */
2467
+ get length() {
2468
+ return Object.keys(this.storage).length;
1984
2469
  }
1985
- register(registered) {
1986
- const { packageName, className } = registered;
1987
- const existingRegistrationIndex = this.storage.findIndex((item) => item.packageName === packageName && item.className === className);
1988
- const existingRegistration = this.storage[existingRegistrationIndex];
1989
- if (!existingRegistration) {
1990
- this.storage.push(registered);
1991
- }
1992
- else {
1993
- this.storage[existingRegistrationIndex] = registered;
1994
- }
1995
- return {
1996
- registerName: this.registerName,
1997
- packageName,
1998
- className,
1999
- get isDestroyed() {
2000
- return false;
2001
- },
2002
- destroy() {
2003
- throw new NotYetImplementedError(`Registration to ${this.registerName} is permanent in this version of Promptbook`);
2004
- },
2005
- };
2470
+ /**
2471
+ * Empties the list associated with the object of all key/value pairs, if there are any.
2472
+ */
2473
+ clear() {
2474
+ this.storage = {};
2475
+ }
2476
+ /**
2477
+ * Returns the current value associated with the given key, or null if the given key does not exist in the list associated with the object.
2478
+ */
2479
+ getItem(key) {
2480
+ return this.storage[key] || null;
2481
+ }
2482
+ /**
2483
+ * Returns the name of the nth key in the list, or null if n is greater than or equal to the number of key/value pairs in the object.
2484
+ */
2485
+ key(index) {
2486
+ return Object.keys(this.storage)[index] || null;
2487
+ }
2488
+ /**
2489
+ * Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.
2490
+ */
2491
+ setItem(key, value) {
2492
+ this.storage[key] = value;
2493
+ }
2494
+ /**
2495
+ * Removes the key/value pair with the given key from the list associated with the object, if a key/value pair with the given key exists.
2496
+ */
2497
+ removeItem(key) {
2498
+ delete this.storage[key];
2006
2499
  }
2007
2500
  }
2008
2501
 
2009
2502
  /**
2010
- * @@@
2503
+ * Simple wrapper `new Date().toISOString()`
2011
2504
  *
2012
- * Note: `$` is used to indicate that this interacts with the global scope
2013
- * @singleton Only one instance of each register is created per build, but thare can be more @@@
2014
- * @public exported from `@promptbook/core`
2015
- */
2016
- const $llmToolsMetadataRegister = new $Register('llm_tools_metadata');
2017
- /**
2018
- * TODO: [®] DRY Register logic
2505
+ * Note: `$` is used to indicate that this function is not a pure function - it is not deterministic because it depends on the current time
2506
+ *
2507
+ * @returns string_date branded type
2508
+ * @public exported from `@promptbook/utils`
2019
2509
  */
2510
+ function $getCurrentDate() {
2511
+ return new Date().toISOString();
2512
+ }
2020
2513
 
2021
2514
  /**
2022
- * Determines if the given path is a root path.
2515
+ * Intercepts LLM tools and counts total usage of the tools
2023
2516
  *
2024
- * Note: This does not check if the file exists only if the path is valid
2025
- * @public exported from `@promptbook/utils`
2517
+ * Note: It can take extended `LlmExecutionTools` and cache the
2518
+ *
2519
+ * @param llmTools LLM tools to be intercepted with usage counting, it can contain extra methods like `totalUsage`
2520
+ * @returns LLM tools with same functionality with added total cost counting
2521
+ * @public exported from `@promptbook/core`
2026
2522
  */
2027
- function isRootPath(value) {
2028
- if (value === '/') {
2029
- return true;
2523
+ function cacheLlmTools(llmTools, options = {}) {
2524
+ const { storage = new MemoryStorage(), isCacheReloaded = false } = options;
2525
+ const proxyTools = {
2526
+ ...llmTools,
2527
+ // <- Note: [🥫]
2528
+ get title() {
2529
+ // TODO: [🧠] Maybe put here some suffix
2530
+ return llmTools.title;
2531
+ },
2532
+ get description() {
2533
+ // TODO: [🧠] Maybe put here some suffix
2534
+ return llmTools.description;
2535
+ },
2536
+ listModels() {
2537
+ // TODO: [🧠] Should be model listing also cached?
2538
+ return /* not await */ llmTools.listModels();
2539
+ },
2540
+ };
2541
+ const callCommonModel = async (prompt) => {
2542
+ const { parameters, content, modelRequirements } = prompt;
2543
+ // <- Note: These are relevant things from the prompt that the cache key should depend on.
2544
+ const key = titleToName(prompt.title.substring(0, MAX_FILENAME_LENGTH - 10) +
2545
+ '-' +
2546
+ sha256__default["default"](hexEncoder__default["default"].parse(JSON.stringify({ parameters, content, modelRequirements }))).toString( /* hex */));
2547
+ const cacheItem = !isCacheReloaded ? await storage.getItem(key) : null;
2548
+ if (cacheItem) {
2549
+ return cacheItem.promptResult;
2550
+ }
2551
+ let promptResult;
2552
+ variant: switch (prompt.modelRequirements.modelVariant) {
2553
+ case 'CHAT':
2554
+ promptResult = await llmTools.callChatModel(prompt);
2555
+ break variant;
2556
+ case 'COMPLETION':
2557
+ promptResult = await llmTools.callCompletionModel(prompt);
2558
+ break variant;
2559
+ case 'EMBEDDING':
2560
+ promptResult = await llmTools.callEmbeddingModel(prompt);
2561
+ break variant;
2562
+ // <- case [🤖]:
2563
+ default:
2564
+ throw new PipelineExecutionError(`Unknown model variant "${prompt.modelRequirements.modelVariant}"`);
2565
+ }
2566
+ // TODO: [🧠] !!5 How to do timing in mixed cache / non-cache situation
2567
+ // promptResult.timing: FromtoItems
2568
+ await storage.setItem(key, {
2569
+ date: $getCurrentDate(),
2570
+ promptbookVersion: PROMPTBOOK_ENGINE_VERSION,
2571
+ prompt,
2572
+ promptResult,
2573
+ });
2574
+ return promptResult;
2575
+ };
2576
+ if (llmTools.callChatModel !== undefined) {
2577
+ proxyTools.callChatModel = async (prompt) => {
2578
+ return /* not await */ callCommonModel(prompt);
2579
+ };
2030
2580
  }
2031
- if (/^[A-Z]:\\$/i.test(value)) {
2032
- return true;
2581
+ if (llmTools.callCompletionModel !== undefined) {
2582
+ proxyTools.callCompletionModel = async (prompt) => {
2583
+ return /* not await */ callCommonModel(prompt);
2584
+ };
2033
2585
  }
2034
- return false;
2586
+ if (llmTools.callEmbeddingModel !== undefined) {
2587
+ proxyTools.callEmbeddingModel = async (prompt) => {
2588
+ return /* not await */ callCommonModel(prompt);
2589
+ };
2590
+ }
2591
+ // <- Note: [🤖]
2592
+ return proxyTools;
2035
2593
  }
2036
2594
  /**
2037
- * TODO: [🍏] Make for MacOS paths
2595
+ * TODO: [🧠][💸] Maybe make some common abstraction `interceptLlmTools` and use here (or use javascript Proxy?)
2596
+ * TODO: [🧠] Is there some meaningfull way how to test this util
2597
+ * TODO: [👷‍♂️] @@@ Manual about construction of llmTools
2598
+ * @@@ write discussion about this and storages
2599
+ * @@@ write how to combine multiple interceptors
2038
2600
  */
2039
2601
 
2040
2602
  /**
2041
- * @@@
2603
+ * Represents the uncertain value
2042
2604
  *
2043
- * Note: `$` is used to indicate that this interacts with the global scope
2044
- * @singleton Only one instance of each register is created per build, but thare can be more @@@
2045
2605
  * @public exported from `@promptbook/core`
2046
2606
  */
2047
- const $llmToolsRegister = new $Register('llm_execution_tools_constructors');
2607
+ const ZERO_VALUE = $deepFreeze({ value: 0 });
2048
2608
  /**
2049
- * TODO: [®] DRY Register logic
2609
+ * Represents the uncertain value
2610
+ *
2611
+ * @public exported from `@promptbook/core`
2050
2612
  */
2051
-
2613
+ const UNCERTAIN_ZERO_VALUE = $deepFreeze({ value: 0, isUncertain: true });
2052
2614
  /**
2053
- * Path to the `.env` file which was used to configure LLM tools
2615
+ * Represents the usage with no resources consumed
2054
2616
  *
2055
- * Note: `$` is used to indicate that this variable is changed by side effect in `$provideLlmToolsConfigurationFromEnv` through `$setUsedEnvFilename`
2617
+ * @public exported from `@promptbook/core`
2056
2618
  */
2057
- let $usedEnvFilename = null;
2619
+ const ZERO_USAGE = $deepFreeze({
2620
+ price: ZERO_VALUE,
2621
+ input: {
2622
+ tokensCount: ZERO_VALUE,
2623
+ charactersCount: ZERO_VALUE,
2624
+ wordsCount: ZERO_VALUE,
2625
+ sentencesCount: ZERO_VALUE,
2626
+ linesCount: ZERO_VALUE,
2627
+ paragraphsCount: ZERO_VALUE,
2628
+ pagesCount: ZERO_VALUE,
2629
+ },
2630
+ output: {
2631
+ tokensCount: ZERO_VALUE,
2632
+ charactersCount: ZERO_VALUE,
2633
+ wordsCount: ZERO_VALUE,
2634
+ sentencesCount: ZERO_VALUE,
2635
+ linesCount: ZERO_VALUE,
2636
+ paragraphsCount: ZERO_VALUE,
2637
+ pagesCount: ZERO_VALUE,
2638
+ },
2639
+ });
2058
2640
  /**
2059
- * Pass the `.env` file which was used to configure LLM tools
2060
- *
2061
- * Note: `$` is used to indicate that this variable is making side effect
2641
+ * Represents the usage with unknown resources consumed
2062
2642
  *
2063
- * @private internal log of `$provideLlmToolsConfigurationFromEnv` and `$registeredLlmToolsMessage`
2643
+ * @public exported from `@promptbook/core`
2064
2644
  */
2065
- function $setUsedEnvFilename(filepath) {
2066
- $usedEnvFilename = filepath;
2067
- }
2645
+ const UNCERTAIN_USAGE = $deepFreeze({
2646
+ price: UNCERTAIN_ZERO_VALUE,
2647
+ input: {
2648
+ tokensCount: UNCERTAIN_ZERO_VALUE,
2649
+ charactersCount: UNCERTAIN_ZERO_VALUE,
2650
+ wordsCount: UNCERTAIN_ZERO_VALUE,
2651
+ sentencesCount: UNCERTAIN_ZERO_VALUE,
2652
+ linesCount: UNCERTAIN_ZERO_VALUE,
2653
+ paragraphsCount: UNCERTAIN_ZERO_VALUE,
2654
+ pagesCount: UNCERTAIN_ZERO_VALUE,
2655
+ },
2656
+ output: {
2657
+ tokensCount: UNCERTAIN_ZERO_VALUE,
2658
+ charactersCount: UNCERTAIN_ZERO_VALUE,
2659
+ wordsCount: UNCERTAIN_ZERO_VALUE,
2660
+ sentencesCount: UNCERTAIN_ZERO_VALUE,
2661
+ linesCount: UNCERTAIN_ZERO_VALUE,
2662
+ paragraphsCount: UNCERTAIN_ZERO_VALUE,
2663
+ pagesCount: UNCERTAIN_ZERO_VALUE,
2664
+ },
2665
+ });
2068
2666
  /**
2069
- * Creates a message with all registered LLM tools
2667
+ * Note: [💞] Ignore a discrepancy between file name and entity name
2668
+ */
2669
+
2670
+ /**
2671
+ * Function `addUsage` will add multiple usages into one
2070
2672
  *
2071
- * Note: This function is used to create a (error) message when there is no constructor for some LLM provider
2673
+ * Note: If you provide 0 values, it returns ZERO_USAGE
2072
2674
  *
2073
- * @private internal function of `createLlmToolsFromConfiguration` and `$provideLlmToolsFromEnv`
2675
+ * @public exported from `@promptbook/core`
2074
2676
  */
2075
- function $registeredLlmToolsMessage() {
2076
- let env;
2077
- if ($isRunningInNode()) {
2078
- env = process.env;
2079
- // <- TODO: [⚛] Some DRY way how to get to `process.env` and pass it into functions - ACRY search for `env`
2080
- }
2081
- else {
2082
- env = {};
2083
- }
2084
- /**
2085
- * Mixes registered LLM tools from $llmToolsMetadataRegister and $llmToolsRegister
2086
- */
2087
- const all = [];
2088
- for (const { title, packageName, className, envVariables } of $llmToolsMetadataRegister.list()) {
2089
- if (all.some((item) => item.packageName === packageName && item.className === className)) {
2090
- continue;
2091
- }
2092
- all.push({ title, packageName, className, envVariables });
2093
- }
2094
- for (const { packageName, className } of $llmToolsRegister.list()) {
2095
- if (all.some((item) => item.packageName === packageName && item.className === className)) {
2096
- continue;
2097
- }
2098
- all.push({ packageName, className });
2099
- }
2100
- const metadata = all.map((metadata) => {
2101
- var _a, _b;
2102
- const isMetadataAviailable = $llmToolsMetadataRegister
2103
- .list()
2104
- .find(({ packageName, className }) => metadata.packageName === packageName && metadata.className === className);
2105
- const isInstalled = $llmToolsRegister
2106
- .list()
2107
- .find(({ packageName, className }) => metadata.packageName === packageName && metadata.className === className);
2108
- const isFullyConfigured = ((_a = metadata.envVariables) === null || _a === void 0 ? void 0 : _a.every((envVariableName) => env[envVariableName] !== undefined)) || false;
2109
- const isPartiallyConfigured = ((_b = metadata.envVariables) === null || _b === void 0 ? void 0 : _b.some((envVariableName) => env[envVariableName] !== undefined)) || false;
2110
- // <- Note: [🗨]
2111
- return { ...metadata, isMetadataAviailable, isInstalled, isFullyConfigured, isPartiallyConfigured };
2112
- });
2113
- const usedEnvMessage = $usedEnvFilename === null ? `Unknown \`.env\` file` : `Used \`.env\` file:\n${$usedEnvFilename}`;
2114
- if (metadata.length === 0) {
2115
- return spaceTrim__default["default"]((block) => `
2116
- No LLM providers are available.
2117
-
2118
- ${block(usedEnvMessage)}
2119
- `);
2120
- }
2121
- return spaceTrim__default["default"]((block) => `
2122
-
2123
- ${block(usedEnvMessage)}
2124
-
2125
- Relevant environment variables:
2126
- ${block(Object.keys(env)
2127
- .filter((envVariableName) => metadata.some(({ envVariables }) => envVariables === null || envVariables === void 0 ? void 0 : envVariables.includes(envVariableName)))
2128
- .map((envVariableName) => `- \`${envVariableName}\``)
2129
- .join('\n'))}
2130
-
2131
- Available LLM providers are:
2132
- ${block(metadata
2133
- .map(({ title, packageName, className, envVariables, isMetadataAviailable, isInstalled, isFullyConfigured, isPartiallyConfigured, }, i) => {
2134
- const morePieces = [];
2135
- if (just(false)) ;
2136
- else if (!isMetadataAviailable && !isInstalled) {
2137
- // TODO: [�][�] Maybe do allow to do auto-install if package not registered and not found
2138
- morePieces.push(`Not installed and no metadata, looks like a unexpected behavior`);
2139
- }
2140
- else if (isMetadataAviailable && !isInstalled) {
2141
- // TODO: [�][�]
2142
- morePieces.push(`Not installed`);
2143
- }
2144
- else if (!isMetadataAviailable && isInstalled) {
2145
- morePieces.push(`No metadata but installed, looks like a unexpected behavior`);
2146
- }
2147
- else if (isMetadataAviailable && isInstalled) {
2148
- morePieces.push(`Installed`);
2149
- }
2150
- else {
2151
- morePieces.push(`unknown state, looks like a unexpected behavior`);
2152
- } /* not else */
2153
- if (isFullyConfigured) {
2154
- morePieces.push(`Configured`);
2155
- }
2156
- else if (isPartiallyConfigured) {
2157
- morePieces.push(`Partially confugured, missing ${envVariables === null || envVariables === void 0 ? void 0 : envVariables.filter((envVariable) => env[envVariable] === undefined).join(' + ')}`);
2158
- }
2159
- else {
2160
- if (envVariables !== null) {
2161
- morePieces.push(`Not configured, to configure set env ${envVariables === null || envVariables === void 0 ? void 0 : envVariables.join(' + ')}`);
2677
+ function addUsage(...usageItems) {
2678
+ return usageItems.reduce((acc, item) => {
2679
+ var _a;
2680
+ acc.price.value += ((_a = item.price) === null || _a === void 0 ? void 0 : _a.value) || 0;
2681
+ for (const key of Object.keys(acc.input)) {
2682
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2683
+ //@ts-ignore
2684
+ if (item.input[key]) {
2685
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2686
+ //@ts-ignore
2687
+ acc.input[key].value += item.input[key].value || 0;
2688
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2689
+ //@ts-ignore
2690
+ if (item.input[key].isUncertain) {
2691
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2692
+ //@ts-ignore
2693
+ acc.input[key].isUncertain = true;
2694
+ }
2695
+ }
2162
2696
  }
2163
- else {
2164
- morePieces.push(`Not configured`); // <- Note: Can not be configured via environment variables
2697
+ for (const key of Object.keys(acc.output)) {
2698
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2699
+ //@ts-ignore
2700
+ if (item.output[key]) {
2701
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2702
+ //@ts-ignore
2703
+ acc.output[key].value += item.output[key].value || 0;
2704
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2705
+ //@ts-ignore
2706
+ if (item.output[key].isUncertain) {
2707
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2708
+ //@ts-ignore
2709
+ acc.output[key].isUncertain = true;
2710
+ }
2711
+ }
2165
2712
  }
2713
+ return acc;
2714
+ }, deepClone(ZERO_USAGE));
2715
+ }
2716
+
2717
+ /**
2718
+ * Intercepts LLM tools and counts total usage of the tools
2719
+ *
2720
+ * @param llmTools LLM tools to be intercepted with usage counting
2721
+ * @returns LLM tools with same functionality with added total cost counting
2722
+ * @public exported from `@promptbook/core`
2723
+ */
2724
+ function countUsage(llmTools) {
2725
+ let totalUsage = ZERO_USAGE;
2726
+ const spending = new rxjs.Subject();
2727
+ const proxyTools = {
2728
+ get title() {
2729
+ // TODO: [🧠] Maybe put here some suffix
2730
+ return llmTools.title;
2731
+ },
2732
+ get description() {
2733
+ // TODO: [🧠] Maybe put here some suffix
2734
+ return llmTools.description;
2735
+ },
2736
+ checkConfiguration() {
2737
+ return /* not await */ llmTools.checkConfiguration();
2738
+ },
2739
+ listModels() {
2740
+ return /* not await */ llmTools.listModels();
2741
+ },
2742
+ spending() {
2743
+ return spending.asObservable();
2744
+ },
2745
+ getTotalUsage() {
2746
+ // <- Note: [🥫] Not using getter `get totalUsage` but `getTotalUsage` to allow this object to be proxied
2747
+ return totalUsage;
2748
+ },
2749
+ };
2750
+ if (llmTools.callChatModel !== undefined) {
2751
+ proxyTools.callChatModel = async (prompt) => {
2752
+ // console.info('[🚕] callChatModel through countTotalUsage');
2753
+ const promptResult = await llmTools.callChatModel(prompt);
2754
+ totalUsage = addUsage(totalUsage, promptResult.usage);
2755
+ spending.next(promptResult.usage);
2756
+ return promptResult;
2757
+ };
2166
2758
  }
2167
- let providerMessage = spaceTrim__default["default"](`
2168
- ${i + 1}) **${title}** \`${className}\` from \`${packageName}\`
2169
- ${morePieces.join('; ')}
2170
- `);
2171
- if ($isRunningInNode) {
2172
- if (isInstalled && isFullyConfigured) {
2173
- providerMessage = colors__default["default"].green(providerMessage);
2174
- }
2175
- else if (isInstalled && isPartiallyConfigured) {
2176
- providerMessage = colors__default["default"].yellow(providerMessage);
2177
- }
2178
- else {
2179
- providerMessage = colors__default["default"].gray(providerMessage);
2180
- }
2759
+ if (llmTools.callCompletionModel !== undefined) {
2760
+ proxyTools.callCompletionModel = async (prompt) => {
2761
+ // console.info('[🚕] callCompletionModel through countTotalUsage');
2762
+ const promptResult = await llmTools.callCompletionModel(prompt);
2763
+ totalUsage = addUsage(totalUsage, promptResult.usage);
2764
+ spending.next(promptResult.usage);
2765
+ return promptResult;
2766
+ };
2181
2767
  }
2182
- return providerMessage;
2183
- })
2184
- .join('\n'))}
2185
- `);
2768
+ if (llmTools.callEmbeddingModel !== undefined) {
2769
+ proxyTools.callEmbeddingModel = async (prompt) => {
2770
+ // console.info('[🚕] callEmbeddingModel through countTotalUsage');
2771
+ const promptResult = await llmTools.callEmbeddingModel(prompt);
2772
+ totalUsage = addUsage(totalUsage, promptResult.usage);
2773
+ spending.next(promptResult.usage);
2774
+ return promptResult;
2775
+ };
2776
+ }
2777
+ // <- Note: [🤖]
2778
+ return proxyTools;
2186
2779
  }
2187
2780
  /**
2188
- * TODO: [®] DRY Register logic
2189
- * TODO: [🧠][⚛] Maybe pass env as argument
2781
+ * TODO: [🧠][💸] Maybe make some common abstraction `interceptLlmTools` and use here (or use javascript Proxy?)
2782
+ * TODO: [🧠] Is there some meaningfull way how to test this util
2783
+ * TODO: [🧠][🌯] Maybe a way how to hide ability to `get totalUsage`
2784
+ * > const [llmToolsWithUsage,getUsage] = countTotalUsage(llmTools);
2785
+ * TODO: [👷‍♂️] @@@ Manual about construction of llmTools
2190
2786
  */
2191
2787
 
2192
2788
  /**
2193
2789
  * @@@
2194
2790
  *
2195
2791
  * @@@ .env
2792
+ * Note: `$` is used to indicate that this function is not a pure function - it uses filesystem to access `.env` file
2196
2793
  *
2197
2794
  * It looks for environment variables:
2198
2795
  * - `process.env.OPENAI_API_KEY`
@@ -2206,33 +2803,9 @@
2206
2803
  if (!$isRunningInNode()) {
2207
2804
  throw new EnvironmentMismatchError('Function `$provideLlmToolsFromEnv` works only in Node.js environment');
2208
2805
  }
2209
- const envFilePatterns = [
2210
- '.env',
2211
- '.env.test',
2212
- '.env.local',
2213
- '.env.development.local',
2214
- '.env.development',
2215
- '.env.production.local',
2216
- '.env.production',
2217
- '.env.prod.local',
2218
- '.env.prod',
2219
- // <- TODO: Maybe add more patterns
2220
- ];
2221
- let rootDirname = process.cwd();
2222
- up_to_root: for (let i = 0; i < LOOP_LIMIT; i++) {
2223
- for (const pattern of envFilePatterns) {
2224
- const envFilename = path.join(rootDirname, pattern);
2225
- if (await isFileExisting(envFilename, $provideFilesystemForNode())) {
2226
- $setUsedEnvFilename(envFilename);
2227
- dotenv__namespace.config({ path: envFilename });
2228
- break up_to_root;
2229
- }
2230
- }
2231
- if (isRootPath(rootDirname)) {
2232
- break up_to_root;
2233
- }
2234
- // Note: If the directory does not exist, try the parent directory
2235
- rootDirname = path.join(rootDirname, '..');
2806
+ const envFilepath = await $provideEnvFilename();
2807
+ if (envFilepath !== null) {
2808
+ dotenv__namespace.config({ path: envFilepath });
2236
2809
  }
2237
2810
  const llmToolsConfiguration = $llmToolsMetadataRegister
2238
2811
  .list()
@@ -2241,15 +2814,8 @@
2241
2814
  return llmToolsConfiguration;
2242
2815
  }
2243
2816
  /**
2244
- * TODO: [🧠][🪁] Maybe do allow to do auto-install if package not registered and not found
2245
- * TODO: Add Azure OpenAI
2246
- * TODO: [🧠][🍛]
2247
- * TODO: [🧠] Is there some meaningfull way how to test this util
2248
2817
  * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
2249
- * TODO: [👷‍♂️] @@@ Manual about construction of llmTools
2250
- * TODO: This should be maybe not under `_common` but under `utils`
2251
- * TODO: [🧠][⚛] Maybe pass env as argument
2252
- * TODO: [®] DRY Register logic */
2818
+ */
2253
2819
 
2254
2820
  /**
2255
2821
  * Multiple LLM Execution Tools is a proxy server that uses multiple execution tools internally and exposes the executor interface externally.
@@ -2414,6 +2980,7 @@
2414
2980
  `);
2415
2981
  // TODO: [🟥] Detect browser / node and make it colorfull
2416
2982
  console.warn(warningMessage);
2983
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
2417
2984
  /*
2418
2985
  return {
2419
2986
  async listModels() {
@@ -2498,6 +3065,7 @@
2498
3065
  * Note: This function is not cached, every call creates new instance of `MultipleLlmExecutionTools`
2499
3066
  *
2500
3067
  * @@@ .env
3068
+ * Note: `$` is used to indicate that this function is not a pure function - it uses filesystem to access `.env` file
2501
3069
  *
2502
3070
  * It looks for environment variables:
2503
3071
  * - `process.env.OPENAI_API_KEY`
@@ -2542,16 +3110,44 @@
2542
3110
  /**
2543
3111
  * Returns LLM tools for CLI
2544
3112
  *
3113
+ * Note: `$` is used to indicate that this function is not a pure function - it uses filesystem to access `.env` file and also writes this .env file
3114
+ *
2545
3115
  * @private within the repository - for CLI utils
2546
3116
  */
2547
3117
  async function $provideLlmToolsForWizzardOrCli(options) {
2548
3118
  if (!$isRunningInNode()) {
2549
3119
  throw new EnvironmentMismatchError('Function `$provideLlmToolsForWizzardOrCli` works only in Node.js environment');
2550
3120
  }
2551
- const { isCacheReloaded } = options !== null && options !== void 0 ? options : {};
2552
- return cacheLlmTools(countTotalUsage(
3121
+ options = options !== null && options !== void 0 ? options : { strategy: 'BRING_YOUR_OWN_KEYS' };
3122
+ const { strategy, isCacheReloaded } = options;
3123
+ let llmExecutionTools;
3124
+ if (strategy === 'REMOTE_SERVER') {
3125
+ const { remoteServerUrl = DEFAULT_REMOTE_SERVER_URL, loginPrompt } = options;
3126
+ const storage = new $EnvStorage(); // <- TODO: !!!!!! Save to `.promptbook` folder
3127
+ let key = `PROMPTBOOK_TOKEN`;
3128
+ if (remoteServerUrl !== DEFAULT_REMOTE_SERVER_URL) {
3129
+ key = `${key}_${remoteServerUrl.replace(/^https?:\/\//i, '')}`;
3130
+ }
3131
+ let identification = await storage.getItem(key);
3132
+ if (identification === null) {
3133
+ identification = await loginPrompt();
3134
+ await storage.setItem(key, identification);
3135
+ }
3136
+ llmExecutionTools = new RemoteLlmExecutionTools({
3137
+ remoteServerUrl,
3138
+ identification,
3139
+ });
3140
+ }
3141
+ else if (strategy === 'BRING_YOUR_OWN_KEYS') {
3142
+ llmExecutionTools = await $provideLlmToolsFromEnv();
3143
+ }
3144
+ else {
3145
+ throw new UnexpectedError(`\`$provideLlmToolsForWizzardOrCli\` wrong strategy "${strategy}"`);
3146
+ }
3147
+ return cacheLlmTools(countUsage(
3148
+ // <- TODO: [🌯] We dont use countUsage at all, maybe just unwrap it
2553
3149
  // <- Note: for example here we don`t want the [🌯]
2554
- await $provideLlmToolsFromEnv()), {
3150
+ llmExecutionTools), {
2555
3151
  storage: new FileCacheStorage({ fs: $provideFilesystemForNode() }, {
2556
3152
  rootFolderPath: path.join(process.cwd(), DEFAULT_EXECUTION_CACHE_DIRNAME),
2557
3153
  }),
@@ -2567,31 +3163,157 @@
2567
3163
  */
2568
3164
 
2569
3165
  /**
2570
- * Just says that the variable is not used but should be kept
2571
- * No side effects.
2572
- *
2573
- * Note: It can be usefull for:
3166
+ * The built-in `fetch' function with a lightweight error handling wrapper as default fetch function used in Promptbook scrapers
2574
3167
  *
2575
- * 1) Suppressing eager optimization of unused imports
2576
- * 2) Suppressing eslint errors of unused variables in the tests
2577
- * 3) Keeping the type of the variable for type testing
3168
+ * @public exported from `@promptbook/core`
3169
+ */
3170
+ const promptbookFetch = async (urlOrRequest, init) => {
3171
+ try {
3172
+ return await fetch(urlOrRequest, init);
3173
+ }
3174
+ catch (error) {
3175
+ if (!(error instanceof Error)) {
3176
+ throw error;
3177
+ }
3178
+ let url;
3179
+ if (typeof urlOrRequest === 'string') {
3180
+ url = urlOrRequest;
3181
+ }
3182
+ else if (urlOrRequest instanceof Request) {
3183
+ url = urlOrRequest.url;
3184
+ }
3185
+ throw new PromptbookFetchError(spaceTrim__default["default"]((block) => `
3186
+ Can not fetch "${url}"
3187
+
3188
+ Fetch error:
3189
+ ${block(error.message)}
3190
+
3191
+ `));
3192
+ }
3193
+ };
3194
+ /**
3195
+ * TODO: [🧠] Maybe rename because it is not used only for scrapers but also in `$getCompiledBook`
3196
+ */
3197
+
3198
+ /**
3199
+ * Checks if value is valid email
2578
3200
  *
2579
- * @param value any values
2580
- * @returns void
2581
- * @private within the repository
3201
+ * @public exported from `@promptbook/utils`
2582
3202
  */
2583
- function keepUnused(...valuesToKeep) {
3203
+ function isValidEmail(email) {
3204
+ if (typeof email !== 'string') {
3205
+ return false;
3206
+ }
3207
+ if (email.split('\n').length > 1) {
3208
+ return false;
3209
+ }
3210
+ return /^.+@.+\..+$/.test(email);
2584
3211
  }
2585
3212
 
2586
3213
  /**
2587
- * Just says that the variable is not used directlys but should be kept because the existence of the variable is important
2588
- *
2589
- * @param value any values
2590
- * @returns void
2591
- * @private within the repository
3214
+ * @private utility of CLI
2592
3215
  */
2593
- function $sideEffect(...sideEffectSubjects) {
2594
- keepUnused(...sideEffectSubjects);
3216
+ function $provideLlmToolsForCli(options) {
3217
+ const { cliOptions: {
3218
+ /* TODO: Use verbose: isVerbose, */ interactive: isInteractive, provider, remoteServerUrl: remoteServerUrlRaw, }, } = options;
3219
+ let strategy;
3220
+ if (/^b/i.test(provider)) {
3221
+ strategy = 'BRING_YOUR_OWN_KEYS';
3222
+ }
3223
+ else if (/^r/i.test(provider)) {
3224
+ strategy = 'REMOTE_SERVER';
3225
+ }
3226
+ else {
3227
+ console.log(colors__default["default"].red(`Unknown provider: "${provider}", please use "BRING_YOUR_OWN_KEYS" or "REMOTE_SERVER"`));
3228
+ process.exit(1);
3229
+ }
3230
+ if (strategy === 'BRING_YOUR_OWN_KEYS') {
3231
+ return /* not await */ $provideLlmToolsForWizzardOrCli({ strategy, ...options });
3232
+ }
3233
+ else if (strategy === 'REMOTE_SERVER') {
3234
+ if (!isValidUrl(remoteServerUrlRaw)) {
3235
+ console.log(colors__default["default"].red(`Invalid URL of remote server: "${remoteServerUrlRaw}"`));
3236
+ process.exit(1);
3237
+ }
3238
+ const remoteServerUrl = remoteServerUrlRaw.endsWith('/') ? remoteServerUrlRaw.slice(0, -1) : remoteServerUrlRaw;
3239
+ return /* not await */ $provideLlmToolsForWizzardOrCli({
3240
+ strategy,
3241
+ appId: CLI_APP_ID,
3242
+ remoteServerUrl,
3243
+ ...options,
3244
+ async loginPrompt() {
3245
+ if (!isInteractive) {
3246
+ console.log(colors__default["default"].red(`You can not login to remote server in non-interactive mode`));
3247
+ process.exit(1);
3248
+ }
3249
+ const { username, password } = await prompts__default["default"]([
3250
+ {
3251
+ type: 'text',
3252
+ name: 'username',
3253
+ message: 'Enter your email:',
3254
+ validate: (value) => (isValidEmail(value) ? true : 'Valid email is required'),
3255
+ },
3256
+ {
3257
+ type: 'password',
3258
+ name: 'password',
3259
+ message: 'Enter your password:',
3260
+ validate: (value) => value.length /* <- TODO: [🧠] Better password validation */ > 0
3261
+ ? true
3262
+ : 'Password is required',
3263
+ },
3264
+ ]);
3265
+ const loginUrl = `${remoteServerUrl}/login`;
3266
+ console.log('!!!', { loginUrl });
3267
+ // TODO: [🧠] Should we use normal `fetch` or `scraperFetch`
3268
+ const response = await promptbookFetch(loginUrl, {
3269
+ method: 'POST',
3270
+ headers: {
3271
+ 'Content-Type': 'application/json',
3272
+ },
3273
+ body: JSON.stringify({
3274
+ appId: CLI_APP_ID,
3275
+ username,
3276
+ password,
3277
+ }),
3278
+ });
3279
+ console.log('!!!', {
3280
+ loginUrl,
3281
+ username,
3282
+ password,
3283
+ // type: response.type,
3284
+ // text: await response.text(),
3285
+ });
3286
+ const { isSuccess, message, error, identification } = (await response.json());
3287
+ console.log('!!!', {
3288
+ isSuccess,
3289
+ message,
3290
+ error,
3291
+ identification,
3292
+ });
3293
+ if (message) {
3294
+ if (isSuccess) {
3295
+ console.log(colors__default["default"].green(message));
3296
+ }
3297
+ else {
3298
+ console.log(colors__default["default"].red(message));
3299
+ }
3300
+ }
3301
+ if (!isSuccess) {
3302
+ // Note: Login failed
3303
+ process.exit(1);
3304
+ }
3305
+ if (!identification) {
3306
+ // Note: Do not get identification here, but server signalizes the success so exiting but with code 0
3307
+ // This can mean for example that user needs to verify email
3308
+ process.exit(0);
3309
+ }
3310
+ return identification;
3311
+ },
3312
+ });
3313
+ }
3314
+ else {
3315
+ throw new UnexpectedError(`\`$provideLlmToolsForCli\` wrong strategy "${strategy}"`);
3316
+ }
2595
3317
  }
2596
3318
 
2597
3319
  /**
@@ -2608,8 +3330,10 @@
2608
3330
  `));
2609
3331
  listModelsCommand.alias('models');
2610
3332
  listModelsCommand.alias('llm');
2611
- listModelsCommand.action(handleActionErrors(async () => {
2612
- const llm = await $provideLlmToolsForWizzardOrCli({});
3333
+ listModelsCommand.action(handleActionErrors(async (cliOptions) => {
3334
+ console.log('!!!', cliOptions);
3335
+ // TODO: !!!!!! Not relevant for remote server and also for `about` command
3336
+ const llm = await $provideLlmToolsForCli({ cliOptions });
2613
3337
  $sideEffect(llm);
2614
3338
  // <- Note: Providing LLM tools will make a side effect of registering all available LLM tools to show the message
2615
3339
  console.info($registeredLlmToolsMessage());
@@ -2707,6 +3431,7 @@
2707
3431
  }
2708
3432
  else {
2709
3433
  console.warn(`Command "${humanReadableCommand}" exceeded time limit of ${timeout}ms but continues running`);
3434
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
2710
3435
  resolve('Command exceeded time limit');
2711
3436
  }
2712
3437
  });
@@ -2732,6 +3457,7 @@
2732
3457
  output.push(stderr.toString());
2733
3458
  if (isVerbose && stderr.toString().trim()) {
2734
3459
  console.warn(stderr.toString());
3460
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
2735
3461
  }
2736
3462
  });
2737
3463
  const finishWithCode = (code) => {
@@ -2743,6 +3469,7 @@
2743
3469
  else {
2744
3470
  if (isVerbose) {
2745
3471
  console.warn(`Command "${humanReadableCommand}" exited with code ${code}`);
3472
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
2746
3473
  }
2747
3474
  resolve(spaceTrim.spaceTrim(output.join('\n')));
2748
3475
  }
@@ -2764,6 +3491,7 @@
2764
3491
  else {
2765
3492
  if (isVerbose) {
2766
3493
  console.warn(error);
3494
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
2767
3495
  }
2768
3496
  resolve(spaceTrim.spaceTrim(output.join('\n')));
2769
3497
  }
@@ -3165,49 +3893,73 @@
3165
3893
  */
3166
3894
 
3167
3895
  /**
3168
- * Converts PipelineCollection to serialized JSON
3169
- *
3170
- * Note: Functions `collectionToJson` and `createCollectionFromJson` are complementary
3896
+ * Initializes `login` command for Promptbook CLI utilities
3171
3897
  *
3172
- * @public exported from `@promptbook/core`
3173
- */
3174
- async function collectionToJson(collection) {
3175
- const pipelineUrls = await collection.listPipelines();
3176
- const promptbooks = await Promise.all(pipelineUrls.map((url) => collection.getPipelineByUrl(url)));
3177
- return promptbooks;
3178
- }
3179
- /**
3180
- * TODO: [🧠] Maybe clear `sourceFile` or clear when exposing through API or remote server
3181
- */
3182
-
3183
- /**
3184
- * This error indicates that the promptbook in a markdown format cannot be parsed into a valid promptbook object
3898
+ * Note: `$` is used to indicate that this function is not a pure function - it registers a command in the CLI
3185
3899
  *
3186
- * @public exported from `@promptbook/core`
3900
+ * @private internal function of `promptbookCli`
3187
3901
  */
3188
- class ParseError extends Error {
3189
- constructor(message) {
3190
- super(message);
3191
- this.name = 'ParseError';
3192
- Object.setPrototypeOf(this, ParseError.prototype);
3193
- }
3902
+ function $initializeLoginCommand(program) {
3903
+ const loginCommand = program.command('login');
3904
+ loginCommand.description(spaceTrim__default["default"](`
3905
+ Login to the remote Promptbook server
3906
+ `));
3907
+ loginCommand.action(handleActionErrors(async () => {
3908
+ // @@@
3909
+ console.error(colors__default["default"].green(spaceTrim__default["default"](`
3910
+ You will be logged in to https://promptbook.studio server.
3911
+ If you don't have an account, it will be created automatically.
3912
+ `)));
3913
+ // !!!!!!!!! Remove from here and use $provideLlmToolsForCli
3914
+ const { email, password } = await prompts__default["default"]([
3915
+ {
3916
+ type: 'text',
3917
+ name: 'email',
3918
+ message: 'Enter your email:',
3919
+ validate: (value) => (isValidEmail(value) ? true : 'Valid email is required'),
3920
+ },
3921
+ {
3922
+ type: 'password',
3923
+ name: 'password',
3924
+ message: 'Enter your password:',
3925
+ validate: (value) => value.length /* <- TODO: [🧠] Better password validation */ > 0 ? true : 'Password is required',
3926
+ },
3927
+ ]);
3928
+ TODO_USE(email, password);
3929
+ await waitasecond.forTime(1000);
3930
+ console.error(colors__default["default"].green(spaceTrim__default["default"](`
3931
+ Your account ${email} was successfully created.
3932
+
3933
+ Please verify your email:
3934
+ https://brj.app/api/v1/customer/register-account?apiKey=PRODdh003eNKaec7PoO1AzU244tsL4WO
3935
+
3936
+ After verification, you will receive 500 000 credits for free 🎉
3937
+ `)));
3938
+ return process.exit(0);
3939
+ }));
3194
3940
  }
3195
3941
  /**
3196
- * TODO: Maybe split `ParseError` and `ApplyError`
3942
+ * TODO: Pass remote server URL (and path)
3943
+ * TODO: Implement non-interactive login
3944
+ * Note: [💞] Ignore a discrepancy between file name and entity name
3945
+ * Note: [🟡] Code in this file should never be published outside of `@promptbook/cli`
3197
3946
  */
3198
3947
 
3199
3948
  /**
3200
- * This error indicates that the promptbook object has valid syntax (=can be parsed) but contains logical errors (like circular dependencies)
3949
+ * Converts PipelineCollection to serialized JSON
3950
+ *
3951
+ * Note: Functions `collectionToJson` and `createCollectionFromJson` are complementary
3201
3952
  *
3202
3953
  * @public exported from `@promptbook/core`
3203
3954
  */
3204
- class PipelineLogicError extends Error {
3205
- constructor(message) {
3206
- super(message);
3207
- this.name = 'PipelineLogicError';
3208
- Object.setPrototypeOf(this, PipelineLogicError.prototype);
3209
- }
3955
+ async function collectionToJson(collection) {
3956
+ const pipelineUrls = await collection.listPipelines();
3957
+ const promptbooks = await Promise.all(pipelineUrls.map((url) => collection.getPipelineByUrl(url)));
3958
+ return promptbooks;
3210
3959
  }
3960
+ /**
3961
+ * TODO: [🧠] Maybe clear `sourceFile` or clear when exposing through API or remote server
3962
+ */
3211
3963
 
3212
3964
  /**
3213
3965
  * Tests if given string is valid semantic version
@@ -3605,21 +4357,6 @@
3605
4357
 
3606
4358
  var PipelineCollection = [{title:"Prepare Knowledge from Markdown",pipelineUrl:"https://promptbook.studio/promptbook/prepare-knowledge-from-markdown.book",formfactorName:"GENERIC",parameters:[{name:"knowledgeContent",description:"Markdown document content",isInput:true,isOutput:false},{name:"knowledgePieces",description:"The knowledge JSON object",isInput:false,isOutput:true}],tasks:[{taskType:"PROMPT_TASK",name:"knowledge",title:"Knowledge",content:"You are experienced data researcher, extract the important knowledge from the document.\n\n# Rules\n\n- Make pieces of information concise, clear, and easy to understand\n- One piece of information should be approximately 1 paragraph\n- Divide the paragraphs by markdown horizontal lines ---\n- Omit irrelevant information\n- Group redundant information\n- Write just extracted information, nothing else\n\n# The document\n\nTake information from this document:\n\n> {knowledgeContent}",resultingParameterName:"knowledgePieces",dependentParameterNames:["knowledgeContent"]}],personas:[],preparations:[],knowledgeSources:[],knowledgePieces:[],sources:[{type:"BOOK",path:null,content:"# Prepare Knowledge from Markdown\n\n- PIPELINE URL `https://promptbook.studio/promptbook/prepare-knowledge-from-markdown.book`\n- INPUT PARAMETER `{knowledgeContent}` Markdown document content\n- OUTPUT PARAMETER `{knowledgePieces}` The knowledge JSON object\n\n## Knowledge\n\n<!-- TODO: [🍆] -FORMAT JSON -->\n\n```markdown\nYou are experienced data researcher, extract the important knowledge from the document.\n\n# Rules\n\n- Make pieces of information concise, clear, and easy to understand\n- One piece of information should be approximately 1 paragraph\n- Divide the paragraphs by markdown horizontal lines ---\n- Omit irrelevant information\n- Group redundant information\n- Write just extracted information, nothing else\n\n# The document\n\nTake information from this document:\n\n> {knowledgeContent}\n```\n\n`-> {knowledgePieces}`\n"}],sourceFile:"./books/prepare-knowledge-from-markdown.book"},{title:"Prepare Keywords",pipelineUrl:"https://promptbook.studio/promptbook/prepare-knowledge-keywords.book",formfactorName:"GENERIC",parameters:[{name:"knowledgePieceContent",description:"The content",isInput:true,isOutput:false},{name:"keywords",description:"Keywords separated by comma",isInput:false,isOutput:true}],tasks:[{taskType:"PROMPT_TASK",name:"knowledge",title:"Knowledge",content:"You are experienced data researcher, detect the important keywords in the document.\n\n# Rules\n\n- Write just keywords separated by comma\n\n# The document\n\nTake information from this document:\n\n> {knowledgePieceContent}",resultingParameterName:"keywords",dependentParameterNames:["knowledgePieceContent"]}],personas:[],preparations:[],knowledgeSources:[],knowledgePieces:[],sources:[{type:"BOOK",path:null,content:"# Prepare Keywords\n\n- PIPELINE URL `https://promptbook.studio/promptbook/prepare-knowledge-keywords.book`\n- INPUT PARAMETER `{knowledgePieceContent}` The content\n- OUTPUT PARAMETER `{keywords}` Keywords separated by comma\n\n## Knowledge\n\n<!-- TODO: [🍆] -FORMAT JSON -->\n\n```markdown\nYou are experienced data researcher, detect the important keywords in the document.\n\n# Rules\n\n- Write just keywords separated by comma\n\n# The document\n\nTake information from this document:\n\n> {knowledgePieceContent}\n```\n\n`-> {keywords}`\n"}],sourceFile:"./books/prepare-knowledge-keywords.book"},{title:"Prepare Knowledge-piece Title",pipelineUrl:"https://promptbook.studio/promptbook/prepare-knowledge-title.book",formfactorName:"GENERIC",parameters:[{name:"knowledgePieceContent",description:"The content",isInput:true,isOutput:false},{name:"title",description:"The title of the document",isInput:false,isOutput:true}],tasks:[{taskType:"PROMPT_TASK",name:"knowledge",title:"Knowledge",content:"You are experienced content creator, write best title for the document.\n\n# Rules\n\n- Write just title, nothing else\n- Write maximum 5 words for the title\n\n# The document\n\n> {knowledgePieceContent}",resultingParameterName:"title",expectations:{words:{min:1,max:8}},dependentParameterNames:["knowledgePieceContent"]}],personas:[],preparations:[],knowledgeSources:[],knowledgePieces:[],sources:[{type:"BOOK",path:null,content:"# Prepare Knowledge-piece Title\n\n- PIPELINE URL `https://promptbook.studio/promptbook/prepare-knowledge-title.book`\n- INPUT PARAMETER `{knowledgePieceContent}` The content\n- OUTPUT PARAMETER `{title}` The title of the document\n\n## Knowledge\n\n- EXPECT MIN 1 WORD\n- EXPECT MAX 8 WORDS\n\n```markdown\nYou are experienced content creator, write best title for the document.\n\n# Rules\n\n- Write just title, nothing else\n- Write maximum 5 words for the title\n\n# The document\n\n> {knowledgePieceContent}\n```\n\n`-> {title}`\n"}],sourceFile:"./books/prepare-knowledge-title.book"},{title:"Prepare Persona",pipelineUrl:"https://promptbook.studio/promptbook/prepare-persona.book",formfactorName:"GENERIC",parameters:[{name:"availableModelNames",description:"List of available model names separated by comma (,)",isInput:true,isOutput:false},{name:"personaDescription",description:"Description of the persona",isInput:true,isOutput:false},{name:"modelRequirements",description:"Specific requirements for the model",isInput:false,isOutput:true}],tasks:[{taskType:"PROMPT_TASK",name:"make-model-requirements",title:"Make modelRequirements",content:"You are experienced AI engineer, you need to create virtual assistant.\nWrite\n\n## Example\n\n```json\n{\n\"modelName\": \"gpt-4o\",\n\"systemMessage\": \"You are experienced AI engineer and helpfull assistant.\",\n\"temperature\": 0.7\n}\n```\n\n## Instructions\n\n- Your output format is JSON object\n- Write just the JSON object, no other text should be present\n- It contains the following keys:\n - `modelName`: The name of the model to use\n - `systemMessage`: The system message to provide context to the model\n - `temperature`: The sampling temperature to use\n\n### Key `modelName`\n\nPick from the following models:\n\n- {availableModelNames}\n\n### Key `systemMessage`\n\nThe system message is used to communicate instructions or provide context to the model at the beginning of a conversation. It is displayed in a different format compared to user messages, helping the model understand its role in the conversation. The system message typically guides the model's behavior, sets the tone, or specifies desired output from the model. By utilizing the system message effectively, users can steer the model towards generating more accurate and relevant responses.\n\nFor example:\n\n> You are an experienced AI engineer and helpful assistant.\n\n> You are a friendly and knowledgeable chatbot.\n\n### Key `temperature`\n\nThe sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit.\n\nYou can pick a value between 0 and 2. For example:\n\n- `0.1`: Low temperature, extremely conservative and deterministic\n- `0.5`: Medium temperature, balanced between conservative and creative\n- `1.0`: High temperature, creative and bit random\n- `1.5`: Very high temperature, extremely creative and often chaotic and unpredictable\n- `2.0`: Maximum temperature, completely random and unpredictable, for some extreme creative use cases\n\n# The assistant\n\nTake this description of the persona:\n\n> {personaDescription}",resultingParameterName:"modelRequirements",format:"JSON",dependentParameterNames:["availableModelNames","personaDescription"]}],personas:[],preparations:[],knowledgeSources:[],knowledgePieces:[],sources:[{type:"BOOK",path:null,content:"# Prepare Persona\n\n- PIPELINE URL `https://promptbook.studio/promptbook/prepare-persona.book`\n- INPUT PARAMETER `{availableModelNames}` List of available model names separated by comma (,)\n- INPUT PARAMETER `{personaDescription}` Description of the persona\n- OUTPUT PARAMETER `{modelRequirements}` Specific requirements for the model\n\n## Make modelRequirements\n\n- FORMAT JSON\n\n```markdown\nYou are experienced AI engineer, you need to create virtual assistant.\nWrite\n\n## Example\n\n\\`\\`\\`json\n{\n\"modelName\": \"gpt-4o\",\n\"systemMessage\": \"You are experienced AI engineer and helpfull assistant.\",\n\"temperature\": 0.7\n}\n\\`\\`\\`\n\n## Instructions\n\n- Your output format is JSON object\n- Write just the JSON object, no other text should be present\n- It contains the following keys:\n - `modelName`: The name of the model to use\n - `systemMessage`: The system message to provide context to the model\n - `temperature`: The sampling temperature to use\n\n### Key `modelName`\n\nPick from the following models:\n\n- {availableModelNames}\n\n### Key `systemMessage`\n\nThe system message is used to communicate instructions or provide context to the model at the beginning of a conversation. It is displayed in a different format compared to user messages, helping the model understand its role in the conversation. The system message typically guides the model's behavior, sets the tone, or specifies desired output from the model. By utilizing the system message effectively, users can steer the model towards generating more accurate and relevant responses.\n\nFor example:\n\n> You are an experienced AI engineer and helpful assistant.\n\n> You are a friendly and knowledgeable chatbot.\n\n### Key `temperature`\n\nThe sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit.\n\nYou can pick a value between 0 and 2. For example:\n\n- `0.1`: Low temperature, extremely conservative and deterministic\n- `0.5`: Medium temperature, balanced between conservative and creative\n- `1.0`: High temperature, creative and bit random\n- `1.5`: Very high temperature, extremely creative and often chaotic and unpredictable\n- `2.0`: Maximum temperature, completely random and unpredictable, for some extreme creative use cases\n\n# The assistant\n\nTake this description of the persona:\n\n> {personaDescription}\n```\n\n`-> {modelRequirements}`\n"}],sourceFile:"./books/prepare-persona.book"},{title:"Prepare Title",pipelineUrl:"https://promptbook.studio/promptbook/prepare-title.book",formfactorName:"GENERIC",parameters:[{name:"book",description:"The book to prepare the title for",isInput:true,isOutput:false},{name:"title",description:"Best title for the book",isInput:false,isOutput:true}],tasks:[{taskType:"PROMPT_TASK",name:"make-title",title:"Make title",content:"Make best title for given text which describes the workflow:\n\n## Rules\n\n- Write just title, nothing else\n- Title should be concise and clear - Write maximum ideally 2 words, maximum 5 words\n- Title starts with emoticon\n- Title should not mention the input and output of the workflow but the main purpose of the workflow\n _For example, not \"✍ Convert Knowledge-piece to title\" but \"✍ Title\"_\n\n## The workflow\n\n> {book}",resultingParameterName:"title",expectations:{words:{min:1,max:8},lines:{min:1,max:1}},dependentParameterNames:["book"]}],personas:[],preparations:[],knowledgeSources:[],knowledgePieces:[],sources:[{type:"BOOK",path:null,content:"# Prepare Title\n\n- PIPELINE URL `https://promptbook.studio/promptbook/prepare-title.book`\n- INPUT PARAMETER `{book}` The book to prepare the title for\n- OUTPUT PARAMETER `{title}` Best title for the book\n\n## Make title\n\n- EXPECT MIN 1 Word\n- EXPECT MAX 8 Words\n- EXPECT EXACTLY 1 Line\n\n```markdown\nMake best title for given text which describes the workflow:\n\n## Rules\n\n- Write just title, nothing else\n- Title should be concise and clear - Write maximum ideally 2 words, maximum 5 words\n- Title starts with emoticon\n- Title should not mention the input and output of the workflow but the main purpose of the workflow\n _For example, not \"✍ Convert Knowledge-piece to title\" but \"✍ Title\"_\n\n## The workflow\n\n> {book}\n```\n\n`-> {title}`\n"}],sourceFile:"./books/prepare-title.book"}];
3607
4359
 
3608
- /**
3609
- * Checks if value is valid email
3610
- *
3611
- * @public exported from `@promptbook/utils`
3612
- */
3613
- function isValidEmail(email) {
3614
- if (typeof email !== 'string') {
3615
- return false;
3616
- }
3617
- if (email.split('\n').length > 1) {
3618
- return false;
3619
- }
3620
- return /^.+@.+\..+$/.test(email);
3621
- }
3622
-
3623
4360
  /**
3624
4361
  * Function isValidJsonString will tell you if the string is valid JSON or not
3625
4362
  *
@@ -3853,32 +4590,6 @@
3853
4590
  * TODO: [🧠] Should be in generated .book.md file GENERATOR_WARNING
3854
4591
  */
3855
4592
 
3856
- /**
3857
- * This error indicates that promptbook not found in the collection
3858
- *
3859
- * @public exported from `@promptbook/core`
3860
- */
3861
- class NotFoundError extends Error {
3862
- constructor(message) {
3863
- super(message);
3864
- this.name = 'NotFoundError';
3865
- Object.setPrototypeOf(this, NotFoundError.prototype);
3866
- }
3867
- }
3868
-
3869
- /**
3870
- * This error indicates errors in referencing promptbooks between each other
3871
- *
3872
- * @public exported from `@promptbook/core`
3873
- */
3874
- class PipelineUrlError extends Error {
3875
- constructor(message) {
3876
- super(message);
3877
- this.name = 'PipelineUrlError';
3878
- Object.setPrototypeOf(this, PipelineUrlError.prototype);
3879
- }
3880
- }
3881
-
3882
4593
  /**
3883
4594
  * Parses the task and returns the list of all parameter names
3884
4595
  *
@@ -4049,24 +4760,6 @@
4049
4760
  return new SimplePipelineCollection(...promptbooks);
4050
4761
  }
4051
4762
 
4052
- /**
4053
- * This error type indicates that some tools are missing for pipeline execution or preparation
4054
- *
4055
- * @public exported from `@promptbook/core`
4056
- */
4057
- class MissingToolsError extends Error {
4058
- constructor(message) {
4059
- super(spaceTrim.spaceTrim((block) => `
4060
- ${block(message)}
4061
-
4062
- Note: You have probbably forgot to provide some tools for pipeline execution or preparation
4063
-
4064
- `));
4065
- this.name = 'MissingToolsError';
4066
- Object.setPrototypeOf(this, MissingToolsError.prototype);
4067
- }
4068
- }
4069
-
4070
4763
  /**
4071
4764
  * Determine if the pipeline is fully prepared
4072
4765
  *
@@ -4132,177 +4825,7 @@
4132
4825
  }
4133
4826
  /**
4134
4827
  * TODO: Type the return type correctly
4135
- */
4136
-
4137
- /**
4138
- * This error indicates problems parsing the format value
4139
- *
4140
- * For example, when the format value is not a valid JSON or CSV
4141
- * This is not thrown directly but in extended classes
4142
- *
4143
- * @public exported from `@promptbook/core`
4144
- */
4145
- class AbstractFormatError extends Error {
4146
- // Note: To allow instanceof do not put here error `name`
4147
- // public readonly name = 'AbstractFormatError';
4148
- constructor(message) {
4149
- super(message);
4150
- Object.setPrototypeOf(this, AbstractFormatError.prototype);
4151
- }
4152
- }
4153
-
4154
- /**
4155
- * This error indicates problem with parsing of CSV
4156
- *
4157
- * @public exported from `@promptbook/core`
4158
- */
4159
- class CsvFormatError extends AbstractFormatError {
4160
- constructor(message) {
4161
- super(message);
4162
- this.name = 'CsvFormatError';
4163
- Object.setPrototypeOf(this, CsvFormatError.prototype);
4164
- }
4165
- }
4166
-
4167
- /**
4168
- * This error indicates that the pipeline collection cannot be propperly loaded
4169
- *
4170
- * @public exported from `@promptbook/core`
4171
- */
4172
- class CollectionError extends Error {
4173
- constructor(message) {
4174
- super(message);
4175
- this.name = 'CollectionError';
4176
- Object.setPrototypeOf(this, CollectionError.prototype);
4177
- }
4178
- }
4179
-
4180
- /**
4181
- * This error occurs when some expectation is not met in the execution of the pipeline
4182
- *
4183
- * @public exported from `@promptbook/core`
4184
- * Note: Do not throw this error, its reserved for `checkExpectations` and `createPipelineExecutor` and public ONLY to be serializable through remote server
4185
- * Note: Always thrown in `checkExpectations` and catched in `createPipelineExecutor` and rethrown as `PipelineExecutionError`
4186
- * Note: This is a kindof subtype of PipelineExecutionError
4187
- */
4188
- class ExpectError extends Error {
4189
- constructor(message) {
4190
- super(message);
4191
- this.name = 'ExpectError';
4192
- Object.setPrototypeOf(this, ExpectError.prototype);
4193
- }
4194
- }
4195
-
4196
- /**
4197
- * This error indicates that the promptbook can not retrieve knowledge from external sources
4198
- *
4199
- * @public exported from `@promptbook/core`
4200
- */
4201
- class KnowledgeScrapeError extends Error {
4202
- constructor(message) {
4203
- super(message);
4204
- this.name = 'KnowledgeScrapeError';
4205
- Object.setPrototypeOf(this, KnowledgeScrapeError.prototype);
4206
- }
4207
- }
4208
-
4209
- /**
4210
- * This error type indicates that some limit was reached
4211
- *
4212
- * @public exported from `@promptbook/core`
4213
- */
4214
- class LimitReachedError extends Error {
4215
- constructor(message) {
4216
- super(message);
4217
- this.name = 'LimitReachedError';
4218
- Object.setPrototypeOf(this, LimitReachedError.prototype);
4219
- }
4220
- }
4221
-
4222
- /**
4223
- * Index of all custom errors
4224
- *
4225
- * @public exported from `@promptbook/core`
4226
- */
4227
- const PROMPTBOOK_ERRORS = {
4228
- AbstractFormatError,
4229
- CsvFormatError,
4230
- CollectionError,
4231
- EnvironmentMismatchError,
4232
- ExpectError,
4233
- KnowledgeScrapeError,
4234
- LimitReachedError,
4235
- MissingToolsError,
4236
- NotFoundError,
4237
- NotYetImplementedError,
4238
- ParseError,
4239
- PipelineExecutionError,
4240
- PipelineLogicError,
4241
- PipelineUrlError,
4242
- UnexpectedError,
4243
- // TODO: [🪑]> VersionMismatchError,
4244
- };
4245
- /**
4246
- * Index of all javascript errors
4247
- *
4248
- * @private for internal usage
4249
- */
4250
- const COMMON_JAVASCRIPT_ERRORS = {
4251
- Error,
4252
- EvalError,
4253
- RangeError,
4254
- ReferenceError,
4255
- SyntaxError,
4256
- TypeError,
4257
- URIError,
4258
- AggregateError,
4259
- /*
4260
- Note: Not widely supported
4261
- > InternalError,
4262
- > ModuleError,
4263
- > HeapError,
4264
- > WebAssemblyCompileError,
4265
- > WebAssemblyRuntimeError,
4266
- */
4267
- };
4268
- /**
4269
- * Index of all errors
4270
- *
4271
- * @private for internal usage
4272
- */
4273
- const ALL_ERRORS = {
4274
- ...PROMPTBOOK_ERRORS,
4275
- ...COMMON_JAVASCRIPT_ERRORS,
4276
- };
4277
- /**
4278
- * Note: [💞] Ignore a discrepancy between file name and entity name
4279
- */
4280
-
4281
- /**
4282
- * Deserializes the error object
4283
- *
4284
- * @public exported from `@promptbook/utils`
4285
- */
4286
- function deserializeError(error) {
4287
- const { name, stack, id } = error; // Added id
4288
- let { message } = error;
4289
- let ErrorClass = ALL_ERRORS[error.name];
4290
- if (ErrorClass === undefined) {
4291
- ErrorClass = Error;
4292
- message = `${name}: ${message}`;
4293
- }
4294
- if (stack !== undefined && stack !== '') {
4295
- message = spaceTrim__default["default"]((block) => `
4296
- ${block(message)}
4297
-
4298
- Original stack trace:
4299
- ${block(stack || '')}
4300
- `);
4301
- }
4302
- const deserializedError = new ErrorClass(message);
4303
- deserializedError.id = id; // Assign id to the error object
4304
- return deserializedError;
4305
- }
4828
+ */
4306
4829
 
4307
4830
  /**
4308
4831
  * Asserts that the execution of a Promptbook is successful
@@ -4317,6 +4840,7 @@
4317
4840
  const { isSuccessful, errors, warnings } = executionResult;
4318
4841
  for (const warning of warnings) {
4319
4842
  console.warn(warning.message);
4843
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
4320
4844
  }
4321
4845
  if (isSuccessful === true) {
4322
4846
  return;
@@ -4454,6 +4978,10 @@
4454
4978
 
4455
4979
  Cannot serialize error with name "${name}"
4456
4980
 
4981
+ Authors of Promptbook probably forgot to add this error into the list of errors:
4982
+ https://github.com/webgptorg/promptbook/blob/main/src/errors/0-index.ts
4983
+
4984
+
4457
4985
  ${block(stack || message)}
4458
4986
 
4459
4987
  `));
@@ -4658,7 +5186,7 @@
4658
5186
  if (parameterNames.has(subparameterName)) {
4659
5187
  parameterNames.delete(subparameterName);
4660
5188
  parameterNames.add(foreach.parameterName);
4661
- // <- TODO: [🚎] Warn/logic error when `subparameterName` not used
5189
+ // <- TODO: [🏮] Warn/logic error when `subparameterName` not used
4662
5190
  }
4663
5191
  }
4664
5192
  }
@@ -6273,6 +6801,7 @@
6273
6801
 
6274
6802
  @see more at https://ptbk.io/prepare-pipeline
6275
6803
  `));
6804
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
6276
6805
  }
6277
6806
  let runCount = 0;
6278
6807
  const pipelineExecutorWithCallback = async (inputParameters, onProgress) => {
@@ -6440,32 +6969,6 @@
6440
6969
  return mimeTypes.extension(value) || null;
6441
6970
  }
6442
6971
 
6443
- /**
6444
- * The built-in `fetch' function with a lightweight error handling wrapper as default fetch function used in Promptbook scrapers
6445
- *
6446
- * @private as default `fetch` function used in Promptbook scrapers
6447
- */
6448
- const scraperFetch = async (url, init) => {
6449
- try {
6450
- return await fetch(url, init);
6451
- }
6452
- catch (error) {
6453
- if (!(error instanceof Error)) {
6454
- throw error;
6455
- }
6456
- throw new KnowledgeScrapeError(spaceTrim__default["default"]((block) => `
6457
- Can not fetch "${url}"
6458
-
6459
- Fetch error:
6460
- ${block(error.message)}
6461
-
6462
- `));
6463
- }
6464
- };
6465
- /**
6466
- * TODO: [🧠] Maybe rename because it is not used only for scrapers but also in `$getCompiledBook`
6467
- */
6468
-
6469
6972
  /**
6470
6973
  * @@@
6471
6974
  *
@@ -6474,7 +6977,7 @@
6474
6977
  async function makeKnowledgeSourceHandler(knowledgeSource, tools, options) {
6475
6978
  // console.log('!! makeKnowledgeSourceHandler', knowledgeSource);
6476
6979
  var _a;
6477
- const { fetch = scraperFetch } = tools;
6980
+ const { fetch = promptbookFetch } = tools;
6478
6981
  const { knowledgeSourceContent } = knowledgeSource;
6479
6982
  let { name } = knowledgeSource;
6480
6983
  const { rootDirname = null,
@@ -6615,63 +7118,73 @@
6615
7118
  const { maxParallelCount = DEFAULT_MAX_PARALLEL_COUNT, rootDirname, isVerbose = DEFAULT_IS_VERBOSE } = options;
6616
7119
  const knowledgePreparedUnflatten = new Array(knowledgeSources.length);
6617
7120
  await forEachAsync(knowledgeSources, { maxParallelCount }, async (knowledgeSource, index) => {
6618
- let partialPieces = null;
6619
- const sourceHandler = await makeKnowledgeSourceHandler(knowledgeSource, tools, { rootDirname, isVerbose });
6620
- const scrapers = arrayableToArray(tools.scrapers);
6621
- for (const scraper of scrapers) {
6622
- if (!scraper.metadata.mimeTypes.includes(sourceHandler.mimeType)
6623
- // <- TODO: [🦔] Implement mime-type wildcards
6624
- ) {
6625
- continue;
6626
- }
6627
- const partialPiecesUnchecked = await scraper.scrape(sourceHandler);
6628
- if (partialPiecesUnchecked !== null) {
6629
- partialPieces = [...partialPiecesUnchecked];
6630
- // <- TODO: [🪓] Here should be no need for spreading new array, just `partialPieces = partialPiecesUnchecked`
6631
- break;
6632
- }
6633
- console.warn(spaceTrim__default["default"]((block) => `
6634
- Cannot scrape knowledge from source despite the scraper \`${scraper.metadata.className}\` supports the mime type "${sourceHandler.mimeType}".
7121
+ try {
7122
+ let partialPieces = null;
7123
+ const sourceHandler = await makeKnowledgeSourceHandler(knowledgeSource, tools, { rootDirname, isVerbose });
7124
+ const scrapers = arrayableToArray(tools.scrapers);
7125
+ for (const scraper of scrapers) {
7126
+ if (!scraper.metadata.mimeTypes.includes(sourceHandler.mimeType)
7127
+ // <- TODO: [🦔] Implement mime-type wildcards
7128
+ ) {
7129
+ continue;
7130
+ }
7131
+ const partialPiecesUnchecked = await scraper.scrape(sourceHandler);
7132
+ if (partialPiecesUnchecked !== null) {
7133
+ partialPieces = [...partialPiecesUnchecked];
7134
+ // <- TODO: [🪓] Here should be no need for spreading new array, just `partialPieces = partialPiecesUnchecked`
7135
+ break;
7136
+ }
7137
+ console.warn(spaceTrim__default["default"]((block) => `
7138
+ Cannot scrape knowledge from source despite the scraper \`${scraper.metadata.className}\` supports the mime type "${sourceHandler.mimeType}".
6635
7139
 
6636
- The source:
6637
- ${block(knowledgeSource.knowledgeSourceContent
6638
- .split('\n')
6639
- .map((line) => `> ${line}`)
6640
- .join('\n'))}
7140
+ The source:
7141
+ ${block(knowledgeSource.knowledgeSourceContent
7142
+ .split('\n')
7143
+ .map((line) => `> ${line}`)
7144
+ .join('\n'))}
6641
7145
 
6642
- ${block($registeredScrapersMessage(scrapers))}
7146
+ ${block($registeredScrapersMessage(scrapers))}
6643
7147
 
6644
7148
 
6645
- `));
6646
- }
6647
- if (partialPieces === null) {
6648
- throw new KnowledgeScrapeError(spaceTrim__default["default"]((block) => `
6649
- Cannot scrape knowledge
7149
+ `));
7150
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
7151
+ }
7152
+ if (partialPieces === null) {
7153
+ throw new KnowledgeScrapeError(spaceTrim__default["default"]((block) => `
7154
+ Cannot scrape knowledge
6650
7155
 
6651
- The source:
6652
- > ${block(knowledgeSource.knowledgeSourceContent
6653
- .split('\n')
6654
- .map((line) => `> ${line}`)
6655
- .join('\n'))}
7156
+ The source:
7157
+ > ${block(knowledgeSource.knowledgeSourceContent
7158
+ .split('\n')
7159
+ .map((line) => `> ${line}`)
7160
+ .join('\n'))}
6656
7161
 
6657
- No scraper found for the mime type "${sourceHandler.mimeType}"
7162
+ No scraper found for the mime type "${sourceHandler.mimeType}"
6658
7163
 
6659
- ${block($registeredScrapersMessage(scrapers))}
7164
+ ${block($registeredScrapersMessage(scrapers))}
6660
7165
 
6661
7166
 
6662
- `));
7167
+ `));
7168
+ }
7169
+ const pieces = partialPieces.map((partialPiece) => ({
7170
+ ...partialPiece,
7171
+ sources: [
7172
+ {
7173
+ name: knowledgeSource.name,
7174
+ // line, column <- TODO: [☀]
7175
+ // <- TODO: [❎]
7176
+ },
7177
+ ],
7178
+ }));
7179
+ knowledgePreparedUnflatten[index] = pieces;
7180
+ }
7181
+ catch (error) {
7182
+ if (!(error instanceof Error)) {
7183
+ throw error;
7184
+ }
7185
+ console.warn(error);
7186
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
6663
7187
  }
6664
- const pieces = partialPieces.map((partialPiece) => ({
6665
- ...partialPiece,
6666
- sources: [
6667
- {
6668
- name: knowledgeSource.name,
6669
- // line, column <- TODO: [☀]
6670
- // <- TODO: [❎]
6671
- },
6672
- ],
6673
- }));
6674
- knowledgePreparedUnflatten[index] = pieces;
6675
7188
  });
6676
7189
  const knowledgePrepared = knowledgePreparedUnflatten.flat();
6677
7190
  return knowledgePrepared;
@@ -6777,7 +7290,7 @@
6777
7290
  // TODO: [🚐] Make arrayable LLMs -> single LLM DRY
6778
7291
  const _llms = arrayableToArray(tools.llm);
6779
7292
  const llmTools = _llms.length === 1 ? _llms[0] : joinLlmExecutionTools(..._llms);
6780
- const llmToolsWithUsage = countTotalUsage(llmTools);
7293
+ const llmToolsWithUsage = countUsage(llmTools);
6781
7294
  // <- TODO: [🌯]
6782
7295
  /*
6783
7296
  TODO: [🧠][🪑][🔃] Should this be done or not
@@ -8496,7 +9009,8 @@
8496
9009
  if ($pipelineJson.defaultModelRequirements[command.key] !== undefined) {
8497
9010
  if ($pipelineJson.defaultModelRequirements[command.key] === command.value) {
8498
9011
  console.warn(`Multiple commands \`MODEL ${command.key} ${command.value}\` in the pipeline head`);
8499
- // <- TODO: [🚎][💩] Some better way how to get warnings from pipeline parsing / logic
9012
+ // <- TODO: [🏮] Some better way how to get warnings from pipeline parsing / logic
9013
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
8500
9014
  }
8501
9015
  else {
8502
9016
  throw new ParseError(spaceTrim__default["default"](`
@@ -8528,6 +9042,7 @@
8528
9042
  modelVariant: 'VARIANT',
8529
9043
  maxTokens: '???',
8530
9044
  }[command.key]} ${command.value}\` in the task "${$taskJson.title || $taskJson.name}"`);
9045
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
8531
9046
  }
8532
9047
  else {
8533
9048
  throw new ParseError(spaceTrim__default["default"](`
@@ -8807,15 +9322,15 @@
8807
9322
  }
8808
9323
  console.warn(spaceTrim__default["default"](`
8809
9324
 
8810
- Persona "${personaName}" is defined multiple times with different description:
9325
+ Persona "${personaName}" is defined multiple times with different description:
8811
9326
 
8812
- First definition:
8813
- ${persona.description}
9327
+ First definition:
9328
+ ${persona.description}
8814
9329
 
8815
- Second definition:
8816
- ${personaDescription}
9330
+ Second definition:
9331
+ ${personaDescription}
8817
9332
 
8818
- `));
9333
+ `));
8819
9334
  persona.description += spaceTrim__default["default"]('\n\n' + personaDescription);
8820
9335
  }
8821
9336
 
@@ -11351,7 +11866,6 @@
11351
11866
  makeCommand.option('--no-validation', `Do not validate logic of pipelines in collection`, true);
11352
11867
  makeCommand.option('--validation', `Types of validations separated by comma (options "logic","imports")`, 'logic,imports');
11353
11868
  makeCommand.option('-r, --reload', `Call LLM models even if same prompt with result is in the cache`, false);
11354
- makeCommand.option('-v, --verbose', `Is output verbose`, false);
11355
11869
  makeCommand.option('-o, --output <path>', spaceTrim__default["default"](`
11356
11870
  Where to save the builded collection
11357
11871
 
@@ -11365,7 +11879,8 @@
11365
11879
  Note: This can be used only with "javascript" or "typescript" format
11366
11880
 
11367
11881
  `), DEFAULT_GET_PIPELINE_COLLECTION_FUNCTION_NAME);
11368
- makeCommand.action(handleActionErrors(async (path$1, { projectName, rootUrl, format, functionName, validation, reload: isCacheReloaded, verbose: isVerbose, output, }) => {
11882
+ makeCommand.action(handleActionErrors(async (path$1, cliOptions) => {
11883
+ const { projectName, rootUrl, format, functionName, validation, reload: isCacheReloaded, verbose: isVerbose, output, } = cliOptions;
11369
11884
  if (!isValidJavascriptName(functionName)) {
11370
11885
  console.error(colors__default["default"].red(`Function name "${functionName}" is not valid javascript name`));
11371
11886
  return process.exit(1);
@@ -11393,7 +11908,10 @@
11393
11908
  isCacheReloaded,
11394
11909
  }; /* <- TODO: ` satisfies PrepareAndScrapeOptions` */
11395
11910
  const fs = $provideFilesystemForNode(prepareAndScrapeOptions);
11396
- const llm = await $provideLlmToolsForWizzardOrCli(prepareAndScrapeOptions);
11911
+ const llm = await $provideLlmToolsForCli({
11912
+ cliOptions,
11913
+ ...prepareAndScrapeOptions,
11914
+ });
11397
11915
  const executables = await $provideExecutablesForNode(prepareAndScrapeOptions);
11398
11916
  const tools = {
11399
11917
  llm,
@@ -11446,6 +11964,7 @@
11446
11964
  : path.join(path$1, `${DEFAULT_PIPELINE_COLLECTION_BASE_FILENAME}.${extension}`);
11447
11965
  if (!output.endsWith(`.${extension}`)) {
11448
11966
  console.warn(colors__default["default"].yellow(`Warning: Extension of output file should be "${extension}"`));
11967
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
11449
11968
  }
11450
11969
  await promises.mkdir(path.dirname(filename), { recursive: true });
11451
11970
  if (typeof content === 'string') {
@@ -11546,6 +12065,7 @@
11546
12065
  }
11547
12066
  if (formats.length > 0) {
11548
12067
  console.warn(colors__default["default"].yellow(`Format ${formats.join(' and ')} is not supported`));
12068
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
11549
12069
  }
11550
12070
  console.info(colors__default["default"].green(`Collection builded successfully`));
11551
12071
  if (isVerbose) {
@@ -11592,7 +12112,8 @@
11592
12112
  `));
11593
12113
  }
11594
12114
  console.warn(`No place where to put the section <!--${sectionName}-->, using the end of the file`);
11595
- // <- TODO: [🚎][💩] Some better way how to get warnings from pipeline parsing / logic
12115
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
12116
+ // <- TODO: [🏮] Some better way how to get warnings from pipeline parsing / logic
11596
12117
  return spaceTrim.spaceTrim((block) => `
11597
12118
  ${block(content)}
11598
12119
 
@@ -11658,8 +12179,8 @@
11658
12179
  // <- TODO: [🧟‍♂️] Unite path to promptbook collection argument
11659
12180
  'Pipelines to prettify as glob pattern');
11660
12181
  prettifyCommand.option('-i, --ignore <glob>', `Ignore as glob pattern`);
11661
- prettifyCommand.option('-v, --verbose', `Is output verbose`, false);
11662
- prettifyCommand.action(handleActionErrors(async (filesGlob, { ignore, verbose: isVerbose }) => {
12182
+ prettifyCommand.action(handleActionErrors(async (filesGlob, cliOptions) => {
12183
+ const { ignore, verbose: isVerbose } = cliOptions;
11663
12184
  const filenames = await glob__default["default"](filesGlob, { ignore });
11664
12185
  // <- TODO: [😶]
11665
12186
  for (const filename of filenames) {
@@ -12260,13 +12781,12 @@
12260
12781
  // TODO: [🧅] DRY command arguments
12261
12782
  runCommand.argument('[pipelineSource]', 'Path to book file OR URL to book file, if not provided it will be asked');
12262
12783
  runCommand.option('-r, --reload', `Call LLM models even if same prompt with result is in the cache`, false);
12263
- runCommand.option('-v, --verbose', `Is output verbose`, false);
12264
- runCommand.option('--no-interactive', `Input is not interactive, if true you need to pass all the input parameters through --json`);
12265
12784
  runCommand.option('--no-formfactor', `When set, behavior of the interactive mode is not changed by the formfactor of the pipeline`);
12266
12785
  runCommand.option('-j, --json <json>', `Pass all or some input parameters as JSON record, if used the output is also returned as JSON`);
12267
12786
  runCommand.option('-s, --save-report <path>', `Save report to file`);
12268
- runCommand.action(handleActionErrors(async (pipelineSource, options) => {
12269
- const { reload: isCacheReloaded, interactive: isInteractive, formfactor: isFormfactorUsed, json, verbose: isVerbose, saveReport, } = options;
12787
+ runCommand.action(handleActionErrors(async (pipelineSource, cliOptions) => {
12788
+ console.log('!!!', cliOptions);
12789
+ const { reload: isCacheReloaded, interactive: isInteractive, formfactor: isFormfactorUsed, json, verbose: isVerbose, saveReport, } = cliOptions;
12270
12790
  if (pipelineSource.includes('-') && normalizeToKebabCase(pipelineSource) === pipelineSource) {
12271
12791
  console.error(colors__default["default"].red(`""${pipelineSource}" is not a valid command or book. See 'ptbk --help'.`));
12272
12792
  return process.exit(1);
@@ -12291,7 +12811,7 @@
12291
12811
  const fs = $provideFilesystemForNode(prepareAndScrapeOptions);
12292
12812
  let llm;
12293
12813
  try {
12294
- llm = await $provideLlmToolsForWizzardOrCli(prepareAndScrapeOptions);
12814
+ llm = await $provideLlmToolsForCli({ cliOptions, ...prepareAndScrapeOptions });
12295
12815
  }
12296
12816
  catch (error) {
12297
12817
  if (!(error instanceof Error)) {
@@ -12346,9 +12866,9 @@
12346
12866
  const tools = {
12347
12867
  llm,
12348
12868
  fs,
12349
- fetch: scraperFetch,
12869
+ fetch: promptbookFetch,
12350
12870
  scrapers: await $provideScrapersForNode({ fs, llm, executables }, prepareAndScrapeOptions),
12351
- script: [new JavascriptExecutionTools(options)],
12871
+ script: [new JavascriptExecutionTools(cliOptions)],
12352
12872
  };
12353
12873
  if (isVerbose) {
12354
12874
  console.info(colors__default["default"].gray('--- Getting the book ---'));
@@ -12512,11 +13032,12 @@
12512
13032
  * @public exported from `@promptbook/remote-server`
12513
13033
  */
12514
13034
  function startRemoteServer(options) {
12515
- const { port, collection, createLlmExecutionTools, isAnonymousModeAllowed, isApplicationModeAllowed, isVerbose = DEFAULT_IS_VERBOSE, } = {
13035
+ const { port, collection, createLlmExecutionTools, isAnonymousModeAllowed, isApplicationModeAllowed, isVerbose = DEFAULT_IS_VERBOSE, login, } = {
12516
13036
  isAnonymousModeAllowed: false,
12517
13037
  isApplicationModeAllowed: false,
12518
13038
  collection: null,
12519
13039
  createLlmExecutionTools: null,
13040
+ login: null,
12520
13041
  ...options,
12521
13042
  };
12522
13043
  // <- TODO: [🦪] Some helper type to be able to use discriminant union types with destructuring
@@ -12583,9 +13104,38 @@
12583
13104
  response.setHeader('X-Powered-By', 'Promptbook engine');
12584
13105
  next();
12585
13106
  });
13107
+ const swaggerOptions = {
13108
+ definition: {
13109
+ openapi: '3.0.0',
13110
+ info: {
13111
+ title: 'Promptbook Remote Server API',
13112
+ version: '1.0.0',
13113
+ description: 'API documentation for the Promptbook Remote Server',
13114
+ },
13115
+ servers: [
13116
+ {
13117
+ url: `http://localhost:${port}${rootPath}`,
13118
+ // <- TODO: !!!!! Probbably: Pass `remoteServerUrl` instead of `port` and `rootPath`
13119
+ },
13120
+ ],
13121
+ },
13122
+ apis: ['./src/remote-server/**/*.ts'], // Adjust path as needed
13123
+ };
13124
+ const swaggerSpec = swaggerJsdoc__default["default"](swaggerOptions);
13125
+ app.use([`/api-docs`, `${rootPath}/api-docs`], swaggerUi__default["default"].serve, swaggerUi__default["default"].setup(swaggerSpec));
12586
13126
  const runningExecutionTasks = [];
12587
13127
  // <- TODO: [🤬] Identify the users
12588
13128
  // TODO: [🧠] Do here some garbage collection of finished tasks
13129
+ /**
13130
+ * @swagger
13131
+ * /:
13132
+ * get:
13133
+ * summary: Get server details
13134
+ * description: Returns details about the Promptbook server.
13135
+ * responses:
13136
+ * 200:
13137
+ * description: Server details in markdown format.
13138
+ */
12589
13139
  app.get(['/', rootPath], async (request, response) => {
12590
13140
  var _a;
12591
13141
  if ((_a = request.url) === null || _a === void 0 ? void 0 : _a.includes('socket.io')) {
@@ -12622,9 +13172,12 @@
12622
13172
 
12623
13173
  ## Paths
12624
13174
 
12625
- ${block(app._router.stack
12626
- .map(({ route }) => (route === null || route === void 0 ? void 0 : route.path) || null)
12627
- .filter((path) => path !== null)
13175
+ ${block([
13176
+ ...app._router.stack
13177
+ .map(({ route }) => (route === null || route === void 0 ? void 0 : route.path) || null)
13178
+ .filter((path) => path !== null),
13179
+ '/api-docs',
13180
+ ]
12628
13181
  .map((path) => `- ${path}`)
12629
13182
  .join('\n'))}
12630
13183
 
@@ -12642,7 +13195,96 @@
12642
13195
  https://github.com/webgptorg/promptbook
12643
13196
  `));
12644
13197
  });
12645
- app.get(`${rootPath}/books`, async (request, response) => {
13198
+ /**
13199
+ * @swagger
13200
+ *
13201
+ * /login:
13202
+ * post:
13203
+ * summary: Login to the server
13204
+ * description: Login to the server and get identification.
13205
+ * requestBody:
13206
+ * required: true
13207
+ * content:
13208
+ * application/json:
13209
+ * schema:
13210
+ * type: object
13211
+ * properties:
13212
+ * username:
13213
+ * type: string
13214
+ * password:
13215
+ * type: string
13216
+ * appId:
13217
+ * type: string
13218
+ * responses:
13219
+ * 200:
13220
+ * description: Successful login
13221
+ * content:
13222
+ * application/json:
13223
+ * schema:
13224
+ * type: object
13225
+ * properties:
13226
+ * identification:
13227
+ * type: object
13228
+ */
13229
+ app.post([`/login`, `${rootPath}/login`], async (request, response) => {
13230
+ if (!isApplicationModeAllowed || login === null) {
13231
+ response.status(400).send('Application mode is not allowed');
13232
+ return;
13233
+ }
13234
+ try {
13235
+ const username = request.body.username;
13236
+ const password = request.body.password;
13237
+ const appId = request.body.appId;
13238
+ const { isSuccess, error, message, identification } = await login({
13239
+ username,
13240
+ password,
13241
+ appId,
13242
+ rawRequest: request,
13243
+ rawResponse: response,
13244
+ });
13245
+ response.status(201).send({
13246
+ isSuccess,
13247
+ message,
13248
+ error: error ? serializeError(error) : undefined,
13249
+ identification,
13250
+ });
13251
+ return;
13252
+ }
13253
+ catch (error) {
13254
+ if (!(error instanceof Error)) {
13255
+ throw error;
13256
+ }
13257
+ if (error instanceof AuthenticationError) {
13258
+ response.status(401).send({
13259
+ isSuccess: false,
13260
+ message: error.message,
13261
+ error: serializeError(error),
13262
+ });
13263
+ }
13264
+ console.warn(`Login function thrown different error than AuthenticationError`, {
13265
+ error,
13266
+ serializedError: serializeError(error),
13267
+ });
13268
+ response.status(400).send({ error: serializeError(error) });
13269
+ }
13270
+ });
13271
+ /**
13272
+ * @swagger
13273
+ * /books:
13274
+ * get:
13275
+ * summary: List all books
13276
+ * description: Returns a list of all available books in the collection.
13277
+ * responses:
13278
+ * 200:
13279
+ * description: A list of books.
13280
+ * content:
13281
+ * application/json:
13282
+ * schema:
13283
+ * type: array
13284
+ * items:
13285
+ * type: string
13286
+ */
13287
+ app.get([`/books`, `${rootPath}/books`], async (request, response) => {
12646
13288
  if (collection === null) {
12647
13289
  response.status(500).send('No collection available');
12648
13290
  return;
@@ -12652,7 +13294,30 @@
12652
13294
  response.send(pipelines);
12653
13295
  });
12654
13296
  // TODO: [🧠] Is it secure / good idea to expose source codes of hosted books
12655
- app.get(`${rootPath}/books/*`, async (request, response) => {
13297
+ /**
13298
+ * @swagger
13299
+ * /books/{bookId}:
13300
+ * get:
13301
+ * summary: Get book content
13302
+ * description: Returns the content of a specific book.
13303
+ * parameters:
13304
+ * - in: path
13305
+ * name: bookId
13306
+ * required: true
13307
+ * schema:
13308
+ * type: string
13309
+ * description: The ID of the book to retrieve.
13310
+ * responses:
13311
+ * 200:
13312
+ * description: The content of the book.
13313
+ * content:
13314
+ * text/markdown:
13315
+ * schema:
13316
+ * type: string
13317
+ * 404:
13318
+ * description: Book not found.
13319
+ */
13320
+ app.get([`/books/*`, `${rootPath}/books/*`], async (request, response) => {
12656
13321
  try {
12657
13322
  if (collection === null) {
12658
13323
  response.status(500).send('No collection nor books available');
@@ -12706,10 +13371,26 @@
12706
13371
  };
12707
13372
  }
12708
13373
  }
12709
- app.get(`${rootPath}/executions`, async (request, response) => {
13374
+ /**
13375
+ * @swagger
13376
+ * /executions:
13377
+ * get:
13378
+ * summary: List all executions
13379
+ * description: Returns a list of all running execution tasks.
13380
+ * responses:
13381
+ * 200:
13382
+ * description: A list of execution tasks.
13383
+ * content:
13384
+ * application/json:
13385
+ * schema:
13386
+ * type: array
13387
+ * items:
13388
+ * type: object
13389
+ */
13390
+ app.get([`/executions`, `${rootPath}/executions`], async (request, response) => {
12710
13391
  response.send(runningExecutionTasks.map((runningExecutionTask) => exportExecutionTask(runningExecutionTask, false)));
12711
13392
  });
12712
- app.get(`${rootPath}/executions/last`, async (request, response) => {
13393
+ app.get([`/executions/last`, `${rootPath}/executions/last`], async (request, response) => {
12713
13394
  // TODO: [🤬] Filter only for user
12714
13395
  if (runningExecutionTasks.length === 0) {
12715
13396
  response.status(404).send('No execution tasks found');
@@ -12718,7 +13399,7 @@
12718
13399
  const lastExecutionTask = runningExecutionTasks[runningExecutionTasks.length - 1];
12719
13400
  response.send(exportExecutionTask(lastExecutionTask, true));
12720
13401
  });
12721
- app.get(`${rootPath}/executions/:taskId`, async (request, response) => {
13402
+ app.get([`/executions/:taskId`, `${rootPath}/executions/:taskId`], async (request, response) => {
12722
13403
  const { taskId } = request.params;
12723
13404
  // TODO: [🤬] Filter only for user
12724
13405
  const executionTask = runningExecutionTasks.find((executionTask) => executionTask.taskId === taskId);
@@ -12730,7 +13411,36 @@
12730
13411
  }
12731
13412
  response.send(exportExecutionTask(executionTask, true));
12732
13413
  });
12733
- app.post(`${rootPath}/executions/new`, async (request, response) => {
13414
+ /**
13415
+ * @swagger
13416
+ * /executions/new:
13417
+ * post:
13418
+ * summary: Start a new execution
13419
+ * description: Starts a new execution task for a given pipeline.
13420
+ * requestBody:
13421
+ * required: true
13422
+ * content:
13423
+ * application/json:
13424
+ * schema:
13425
+ * type: object
13426
+ * properties:
13427
+ * pipelineUrl:
13428
+ * type: string
13429
+ * inputParameters:
13430
+ * type: object
13431
+ * identification:
13432
+ * type: object
13433
+ * responses:
13434
+ * 200:
13435
+ * description: The newly created execution task.
13436
+ * content:
13437
+ * application/json:
13438
+ * schema:
13439
+ * type: object
13440
+ * 400:
13441
+ * description: Invalid input.
13442
+ */
13443
+ app.post([`/executions/new`, `${rootPath}/executions/new`], async (request, response) => {
12734
13444
  try {
12735
13445
  const { inputParameters, identification /* <- [🤬] */ } = request.body;
12736
13446
  const pipelineUrl = request.body.pipelineUrl || request.body.book;
@@ -12907,6 +13617,15 @@
12907
13617
  }
12908
13618
  let isDestroyed = false;
12909
13619
  return {
13620
+ get httpServer() {
13621
+ return httpServer;
13622
+ },
13623
+ get expressApp() {
13624
+ return app;
13625
+ },
13626
+ get socketIoServer() {
13627
+ return server;
13628
+ },
12910
13629
  get isDestroyed() {
12911
13630
  return isDestroyed;
12912
13631
  },
@@ -12957,12 +13676,12 @@
12957
13676
  `));
12958
13677
  startServerCommand.option('--allow-anonymous', `Is anonymous mode allowed`, false);
12959
13678
  startServerCommand.option('-r, --reload', `Call LLM models even if same prompt with result is in the cache`, false);
12960
- startServerCommand.option('-v, --verbose', `Is output verbose`, false);
12961
13679
  startServerCommand.description(spaceTrim__default["default"](`
12962
13680
  Starts a remote server to execute books
12963
13681
  `));
12964
13682
  startServerCommand.alias('server');
12965
- startServerCommand.action(handleActionErrors(async (path, { port: portRaw, url: rawUrl, allowAnonymous: isAnonymousModeAllowed, reload: isCacheReloaded, verbose: isVerbose, }) => {
13683
+ startServerCommand.action(handleActionErrors(async (path, cliOptions) => {
13684
+ const { port: portRaw, url: rawUrl, allowAnonymous: isAnonymousModeAllowed, reload: isCacheReloaded, verbose: isVerbose, } = cliOptions;
12966
13685
  if (rawUrl && !isValidUrl(rawUrl)) {
12967
13686
  console.error(colors__default["default"].red(`Invalid URL: ${rawUrl}`));
12968
13687
  return process.exit(1);
@@ -12975,6 +13694,7 @@
12975
13694
  const url = !rawUrl ? null : new URL(rawUrl);
12976
13695
  if (url !== null && url.port !== port.toString()) {
12977
13696
  console.warn(colors__default["default"].yellow(`Port in --url is different from --port which the server will listen on, this is ok only if you proxy from one port to another, for exaple via nginx or docker`));
13697
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
12978
13698
  }
12979
13699
  let rootUrl = undefined;
12980
13700
  if (url !== null) {
@@ -12990,7 +13710,7 @@
12990
13710
  isCacheReloaded,
12991
13711
  }; /* <- TODO: ` satisfies PrepareAndScrapeOptions` */
12992
13712
  const fs = $provideFilesystemForNode(prepareAndScrapeOptions);
12993
- const llm = await $provideLlmToolsForWizzardOrCli(prepareAndScrapeOptions);
13713
+ const llm = await $provideLlmToolsForCli({ cliOptions, ...prepareAndScrapeOptions });
12994
13714
  const executables = await $provideExecutablesForNode(prepareAndScrapeOptions);
12995
13715
  const tools = {
12996
13716
  llm,
@@ -13014,6 +13734,9 @@
13014
13734
  isAnonymousModeAllowed,
13015
13735
  isApplicationModeAllowed: true,
13016
13736
  collection,
13737
+ async login() {
13738
+ throw new AuthenticationError('You can not login to the server started by `ptbk start-server` in cli, use `startRemoteServer` function instead.');
13739
+ },
13017
13740
  createLlmExecutionTools(options) {
13018
13741
  const { appId, userId } = options;
13019
13742
  TODO_USE({ appId, userId });
@@ -13050,8 +13773,8 @@
13050
13773
  testCommand.option('--no-validation', `Do not validate logic of pipelines in collection`, true);
13051
13774
  testCommand.option('--no-prepare', `Do not prepare the pipelines, ideal when no LLM tools or scrapers available`, true);
13052
13775
  testCommand.option('-r, --reload', `Call LLM models even if same prompt with result is in the cache `, false);
13053
- testCommand.option('-v, --verbose', `Is output verbose`, false);
13054
- testCommand.action(handleActionErrors(async (filesGlob, { ignore: ignoreRaw = '', validation: isValidated, prepare: isPrepared, reload: isCacheReloaded, verbose: isVerbose, }) => {
13776
+ testCommand.action(handleActionErrors(async (filesGlob, cliOptions) => {
13777
+ const { ignore: ignoreRaw = '', validation: isValidated, prepare: isPrepared, reload: isCacheReloaded, verbose: isVerbose, } = cliOptions;
13055
13778
  let tools = undefined;
13056
13779
  if (isPrepared) {
13057
13780
  // TODO: DRY [◽]
@@ -13060,7 +13783,7 @@
13060
13783
  isCacheReloaded,
13061
13784
  }; /* <- TODO: ` satisfies PrepareAndScrapeOptions` */
13062
13785
  const fs = $provideFilesystemForNode(prepareAndScrapeOptions);
13063
- const llm = await $provideLlmToolsForWizzardOrCli(prepareAndScrapeOptions);
13786
+ const llm = await $provideLlmToolsForCli({ cliOptions, ...prepareAndScrapeOptions });
13064
13787
  const executables = await $provideExecutablesForNode(prepareAndScrapeOptions);
13065
13788
  tools = {
13066
13789
  llm,
@@ -13120,6 +13843,18 @@
13120
13843
  * Note: This is named "test-command.ts" to avoid name collision with jest unit test files
13121
13844
  */
13122
13845
 
13846
+ /**
13847
+ * Note: `$` is used to indicate that this function is not a pure function - it registers an option in the CLI
13848
+ *
13849
+ * @private utility of CLI
13850
+ */
13851
+ function $addGlobalOptionsToCommand(command) {
13852
+ command.option('-v, --verbose', `Log more details`, false);
13853
+ command.option('--no-interactive', `Input is not interactive, if true, no CLI input is prompted if required, so either defaults are used or the command fails with exit code 1`);
13854
+ command.option('-p, --provider <provider>', `Which LLM provider to use: "BYOK" / "BRING_YOUR_OWN_KEYS" or "REMOTE_SERVER" / "RS"`, 'REMOTE_SERVER');
13855
+ command.option('--remote-server-url <url>', `URL of remote server to use when `, DEFAULT_REMOTE_SERVER_URL);
13856
+ }
13857
+
13123
13858
  /**
13124
13859
  * Runs CLI utilities of Promptbook package
13125
13860
  *
@@ -13144,8 +13879,10 @@
13144
13879
  program.alias('ptbk');
13145
13880
  program.version(PROMPTBOOK_ENGINE_VERSION);
13146
13881
  program.description(CLAIM);
13882
+ // Note: Theese options are valid for all commands
13147
13883
  $initializeAboutCommand(program);
13148
13884
  $initializeRunCommand(program);
13885
+ $initializeLoginCommand(program);
13149
13886
  $initializeHelloCommand(program);
13150
13887
  $initializeMakeCommand(program);
13151
13888
  $initializePrettifyCommand(program);
@@ -13153,6 +13890,8 @@
13153
13890
  $initializeListModelsCommand(program);
13154
13891
  $initializeListScrapersCommand(program);
13155
13892
  $initializeStartServerCommand(program);
13893
+ // TODO: [🧠] Should it be here or not> $addGlobalOptionsToCommand(program);
13894
+ program.commands.forEach($addGlobalOptionsToCommand);
13156
13895
  program.parse(process.argv);
13157
13896
  }
13158
13897
  /**
@@ -13202,8 +13941,7 @@
13202
13941
  options: {
13203
13942
  apiKey: 'sk-ant-api03-',
13204
13943
  isProxied: true,
13205
- remoteUrl: DEFAULT_REMOTE_URL,
13206
- path: DEFAULT_REMOTE_URL_PATH,
13944
+ remoteServerUrl: DEFAULT_REMOTE_SERVER_URL,
13207
13945
  },
13208
13946
  };
13209
13947
  },
@@ -13226,146 +13964,6 @@
13226
13964
  * Note: [💞] Ignore a discrepancy between file name and entity name
13227
13965
  */
13228
13966
 
13229
- /**
13230
- * Creates a connection to the remote proxy server.
13231
- *
13232
- * Note: This function creates a connection to the remote server and returns a socket but responsibility of closing the connection is on the caller
13233
- *
13234
- * @private internal utility function
13235
- */
13236
- async function createRemoteClient(options) {
13237
- const { remoteUrl, path } = options;
13238
- return new Promise((resolve, reject) => {
13239
- const socket = socket_ioClient.io(remoteUrl, {
13240
- retries: CONNECTION_RETRIES_LIMIT,
13241
- timeout: CONNECTION_TIMEOUT_MS,
13242
- path,
13243
- // path: `${this.remoteUrl.pathname}/socket.io`,
13244
- transports: [/*'websocket', <- TODO: [🌬] Make websocket transport work */ 'polling'],
13245
- });
13246
- // console.log('Connecting to', this.options.remoteUrl.href, { socket });
13247
- socket.on('connect', () => {
13248
- resolve(socket);
13249
- });
13250
- // TODO: [💩] Better timeout handling
13251
- setTimeout(() => {
13252
- reject(new Error(`Timeout while connecting to ${remoteUrl}`));
13253
- }, CONNECTION_TIMEOUT_MS);
13254
- });
13255
- }
13256
-
13257
- /**
13258
- * Remote server is a proxy server that uses its execution tools internally and exposes the executor interface externally.
13259
- *
13260
- * You can simply use `RemoteExecutionTools` on client-side javascript and connect to your remote server.
13261
- * This is useful to make all logic on browser side but not expose your API keys or no need to use customer's GPU.
13262
- *
13263
- * @see https://github.com/webgptorg/promptbook#remote-server
13264
- * @public exported from `@promptbook/remote-client`
13265
- */
13266
- class RemoteLlmExecutionTools {
13267
- /* <- TODO: [🍚] `, Destroyable` */
13268
- constructor(options) {
13269
- this.options = options;
13270
- }
13271
- get title() {
13272
- // TODO: [🧠] Maybe fetch title+description from the remote server (as well as if model methods are defined)
13273
- return 'Remote server';
13274
- }
13275
- get description() {
13276
- return 'Use all models by your remote server';
13277
- }
13278
- /**
13279
- * Check the configuration of all execution tools
13280
- */
13281
- async checkConfiguration() {
13282
- const socket = await createRemoteClient(this.options);
13283
- socket.disconnect();
13284
- // TODO: [main] !!3 Check version of the remote server and compatibility
13285
- // TODO: [🎍] Send checkConfiguration
13286
- }
13287
- /**
13288
- * List all available models that can be used
13289
- */
13290
- async listModels() {
13291
- // TODO: [👒] Listing models (and checking configuration) probbably should go through REST API not Socket.io
13292
- const socket = await createRemoteClient(this.options);
13293
- socket.emit('listModels-request', {
13294
- identification: this.options.identification,
13295
- } /* <- Note: [🤛] */);
13296
- const promptResult = await new Promise((resolve, reject) => {
13297
- socket.on('listModels-response', (response) => {
13298
- resolve(response.models);
13299
- socket.disconnect();
13300
- });
13301
- socket.on('error', (error) => {
13302
- reject(deserializeError(error));
13303
- socket.disconnect();
13304
- });
13305
- });
13306
- socket.disconnect();
13307
- return promptResult;
13308
- }
13309
- /**
13310
- * Calls remote proxy server to use a chat model
13311
- */
13312
- callChatModel(prompt) {
13313
- if (this.options.isVerbose) {
13314
- console.info(`🖋 Remote callChatModel call`);
13315
- }
13316
- return /* not await */ this.callCommonModel(prompt);
13317
- }
13318
- /**
13319
- * Calls remote proxy server to use a completion model
13320
- */
13321
- callCompletionModel(prompt) {
13322
- if (this.options.isVerbose) {
13323
- console.info(`💬 Remote callCompletionModel call`);
13324
- }
13325
- return /* not await */ this.callCommonModel(prompt);
13326
- }
13327
- /**
13328
- * Calls remote proxy server to use a embedding model
13329
- */
13330
- callEmbeddingModel(prompt) {
13331
- if (this.options.isVerbose) {
13332
- console.info(`💬 Remote callEmbeddingModel call`);
13333
- }
13334
- return /* not await */ this.callCommonModel(prompt);
13335
- }
13336
- // <- Note: [🤖] callXxxModel
13337
- /**
13338
- * Calls remote proxy server to use both completion or chat model
13339
- */
13340
- async callCommonModel(prompt) {
13341
- const socket = await createRemoteClient(this.options);
13342
- socket.emit('prompt-request', {
13343
- identification: this.options.identification,
13344
- prompt,
13345
- } /* <- Note: [🤛] */);
13346
- const promptResult = await new Promise((resolve, reject) => {
13347
- socket.on('prompt-response', (response) => {
13348
- resolve(response.promptResult);
13349
- socket.disconnect();
13350
- });
13351
- socket.on('error', (error) => {
13352
- reject(deserializeError(error));
13353
- socket.disconnect();
13354
- });
13355
- });
13356
- socket.disconnect();
13357
- return promptResult;
13358
- }
13359
- }
13360
- /**
13361
- * TODO: Maybe use `$exportJson`
13362
- * TODO: [🧠][🛍] Maybe not `isAnonymous: boolean` BUT `mode: 'ANONYMOUS'|'COLLECTION'`
13363
- * TODO: [🍓] Allow to list compatible models with each variant
13364
- * TODO: [🗯] RemoteLlmExecutionTools should extend Destroyable and implement IDestroyable
13365
- * TODO: [🧠][🌰] Allow to pass `title` for tracking purposes
13366
- * TODO: [🧠] Maybe remove `@promptbook/remote-client` and just use `@promptbook/core`
13367
- */
13368
-
13369
13967
  /**
13370
13968
  * Function computeUsage will create price per one token based on the string value found on openai page
13371
13969
  *
@@ -13466,7 +14064,7 @@
13466
14064
  * Helper of usage compute
13467
14065
  *
13468
14066
  * @param content the content of prompt or response
13469
- * @returns part of PromptResultUsageCounts
14067
+ * @returns part of UsageCounts
13470
14068
  *
13471
14069
  * @private internal utility of LlmExecutionTools
13472
14070
  */
@@ -13490,7 +14088,7 @@
13490
14088
  */
13491
14089
  function uncertainNumber(value) {
13492
14090
  if (value === null || value === undefined || Number.isNaN(value)) {
13493
- return { value: 0, isUncertain: true };
14091
+ return UNCERTAIN_ZERO_VALUE;
13494
14092
  }
13495
14093
  return { value };
13496
14094
  }
@@ -13727,7 +14325,7 @@
13727
14325
  const resultContent = rawResponse.choices[0].text;
13728
14326
  // eslint-disable-next-line prefer-const
13729
14327
  complete = $getCurrentDate();
13730
- const usage = { price: 'UNKNOWN', inputTokens: 0, outputTokens: 0 /* <- TODO: [🐞] Compute usage * / } satisfies PromptResultUsage;
14328
+ const usage = { price: 'UNKNOWN', inputTokens: 0, outputTokens: 0 /* <- TODO: [🐞] Compute usage * / } satisfies Usage;
13731
14329
 
13732
14330
 
13733
14331
 
@@ -14823,11 +15421,17 @@
14823
15421
  throw new PipelineExecutionError('No response message');
14824
15422
  }
14825
15423
  const complete = $getCurrentDate();
14826
- /*
14827
- TODO: [🕘] Usage count
14828
- const usage = computeOpenAiUsage(content || '', resultContent || '', rawResponse);
14829
- */
14830
- const usage = UNCERTAIN_USAGE;
15424
+ const usage = {
15425
+ price: UNCERTAIN_ZERO_VALUE,
15426
+ input: {
15427
+ tokensCount: uncertainNumber(rawResponse.usage.promptTokens),
15428
+ ...computeUsageCounts(rawPromptContent),
15429
+ },
15430
+ output: {
15431
+ tokensCount: uncertainNumber(rawResponse.usage.completionTokens),
15432
+ ...computeUsageCounts(rawResponse.text),
15433
+ },
15434
+ };
14831
15435
  return exportJson({
14832
15436
  name: 'promptResult',
14833
15437
  message: `Result of \`createExecutionToolsFromVercelProvider.callChatModel\``,
@@ -15180,15 +15784,18 @@
15180
15784
  }
15181
15785
  return this.client;
15182
15786
  }
15787
+ /*
15788
+ Note: Commenting this out to avoid circular dependency
15183
15789
  /**
15184
15790
  * Create (sub)tools for calling OpenAI API Assistants
15185
15791
  *
15186
15792
  * @param assistantId Which assistant to use
15187
15793
  * @returns Tools for calling OpenAI API Assistants with same token
15188
- */
15189
- createAssistantSubtools(assistantId) {
15794
+ * /
15795
+ public createAssistantSubtools(assistantId: string_token): OpenAiAssistantExecutionTools {
15190
15796
  return new OpenAiAssistantExecutionTools({ ...this.options, assistantId });
15191
15797
  }
15798
+ */
15192
15799
  /**
15193
15800
  * Check the `options` passed to `constructor`
15194
15801
  */