@promptbook/cli 0.89.0-5 โ†’ 0.89.0-7

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.
package/esm/index.es.js CHANGED
@@ -2,11 +2,13 @@ import colors from 'colors';
2
2
  import commander from 'commander';
3
3
  import spaceTrim, { spaceTrim as spaceTrim$1 } from 'spacetrim';
4
4
  import { forTime, forEver } from 'waitasecond';
5
+ import prompts from 'prompts';
5
6
  import { basename, join, dirname, relative } from 'path';
6
7
  import { stat, access, constants, readFile, writeFile, readdir, mkdir, unlink, rm, rename, rmdir } from 'fs/promises';
7
8
  import hexEncoder from 'crypto-js/enc-hex';
8
9
  import sha256 from 'crypto-js/sha256';
9
10
  import { randomBytes } from 'crypto';
11
+ import { io } from 'socket.io-client';
10
12
  import { Subject } from 'rxjs';
11
13
  import * as dotenv from 'dotenv';
12
14
  import { spawn } from 'child_process';
@@ -17,12 +19,12 @@ import { parse, unparse } from 'papaparse';
17
19
  import { SHA256 } from 'crypto-js';
18
20
  import { lookup, extension } from 'mime-types';
19
21
  import glob from 'glob-promise';
20
- import prompts from 'prompts';
21
22
  import moment from 'moment';
22
23
  import express from 'express';
23
24
  import http from 'http';
24
25
  import { Server } from 'socket.io';
25
- import { io } from 'socket.io-client';
26
+ import swaggerJsdoc from 'swagger-jsdoc';
27
+ import swaggerUi from 'swagger-ui-express';
26
28
  import Anthropic from '@anthropic-ai/sdk';
27
29
  import { OpenAIClient, AzureKeyCredential } from '@azure/openai';
28
30
  import OpenAI from 'openai';
@@ -44,7 +46,7 @@ const BOOK_LANGUAGE_VERSION = '1.0.0';
44
46
  * @generated
45
47
  * @see https://github.com/webgptorg/promptbook
46
48
  */
47
- const PROMPTBOOK_ENGINE_VERSION = '0.89.0-5';
49
+ const PROMPTBOOK_ENGINE_VERSION = '0.89.0-7';
48
50
  /**
49
51
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
50
52
  * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
@@ -213,6 +215,7 @@ const DEFAULT_MAX_EXECUTION_ATTEMPTS = 10; // <- TODO: [๐Ÿคนโ€โ™‚๏ธ]
213
215
  */
214
216
  const DEFAULT_BOOKS_DIRNAME = './books';
215
217
  // <- TODO: [๐Ÿ•] Make also `BOOKS_DIRNAME_ALTERNATIVES`
218
+ // TODO: !!!!!! Just .promptbook dir, hardocode others
216
219
  /**
217
220
  * Where to store the temporary downloads
218
221
  *
@@ -237,6 +240,21 @@ const DEFAULT_EXECUTION_CACHE_DIRNAME = './.promptbook/execution-cache';
237
240
  * @public exported from `@promptbook/core`
238
241
  */
239
242
  const DEFAULT_SCRAPE_CACHE_DIRNAME = './.promptbook/scrape-cache';
243
+ /**
244
+ * Id of application for the CLI when using remote server
245
+ *
246
+ * @public exported from `@promptbook/core`
247
+ */
248
+ const CLI_APP_ID = 'cli';
249
+ /*
250
+ TODO: [๐ŸŒƒ]
251
+ /**
252
+ * Id of application for the wizzard when using remote server
253
+ *
254
+ * @public exported from `@promptbook/core`
255
+ * /
256
+ ex-port const WIZZARD_APP_ID: string_app_id = 'wizzard';
257
+ */
240
258
  /**
241
259
  * The name of the builded pipeline collection made by CLI `ptbk make` and for lookup in `createCollectionFromDirectory`
242
260
  *
@@ -257,13 +275,7 @@ const MOMENT_ARG_THRESHOLDS = {
257
275
  *
258
276
  * @public exported from `@promptbook/core`
259
277
  */
260
- const DEFAULT_REMOTE_URL = 'https://api.pavolhejny.com/';
261
- /**
262
- * @@@
263
- *
264
- * @public exported from `@promptbook/core`
265
- */
266
- const DEFAULT_REMOTE_URL_PATH = '/promptbook/socket.io';
278
+ const DEFAULT_REMOTE_SERVER_URL = 'https://api.pavolhejny.com/promptbook';
267
279
  // <- TODO: [๐Ÿงœโ€โ™‚๏ธ]
268
280
  /**
269
281
  * @@@
@@ -305,7 +317,7 @@ const IS_PIPELINE_LOGIC_VALIDATED = just(
305
317
  true);
306
318
  /**
307
319
  * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
308
- * TODO: [๐Ÿง ][๐Ÿงœโ€โ™‚๏ธ] Maybe join remoteUrl and path into single value
320
+ * TODO: [๐Ÿง ][๐Ÿงœโ€โ™‚๏ธ] Maybe join remoteServerUrl and path into single value
309
321
  */
310
322
 
311
323
  /**
@@ -483,7 +495,8 @@ function $initializeHelloCommand(program) {
483
495
  helloCommand.alias('hi');
484
496
  helloCommand.argument('[name]', 'Your name', 'Paul');
485
497
  helloCommand.option('-g, --greeting <greeting>', `Greeting`, 'Hello');
486
- helloCommand.action(handleActionErrors(async (name, { greeting }) => {
498
+ helloCommand.action(handleActionErrors(async (name, cliOptions) => {
499
+ const { greeting } = cliOptions;
487
500
  console.info(colors.cyan(`${greeting} ${name}`));
488
501
  await forTime(1000);
489
502
  console.info(colors.rainbow(`Nice to meet you!`));
@@ -497,40 +510,27 @@ function $initializeHelloCommand(program) {
497
510
  */
498
511
 
499
512
  /**
500
- * Just marks a place of place where should be something implemented
501
- * No side effects.
502
- *
503
- * Note: It can be usefull suppressing eslint errors of unused variables
513
+ * This error type indicates that some part of the code is not implemented yet
504
514
  *
505
- * @param value any values
506
- * @returns void
507
- * @private within the repository
515
+ * @public exported from `@promptbook/core`
508
516
  */
509
- function TODO_USE(...value) {
510
- }
517
+ class NotYetImplementedError extends Error {
518
+ constructor(message) {
519
+ super(spaceTrim$1((block) => `
520
+ ${block(message)}
511
521
 
512
- /**
513
- * @@@
514
- *
515
- * @public exported from `@promptbook/node`
516
- */
517
- function $provideFilesystemForNode(options) {
518
- if (!$isRunningInNode()) {
519
- throw new EnvironmentMismatchError('Function `$provideFilesystemForNode` works only in Node.js environment');
522
+ Note: This feature is not implemented yet but it will be soon.
523
+
524
+ If you want speed up the implementation or just read more, look here:
525
+ https://github.com/webgptorg/promptbook
526
+
527
+ Or contact us on pavol@ptbk.io
528
+
529
+ `));
530
+ this.name = 'NotYetImplementedError';
531
+ Object.setPrototypeOf(this, NotYetImplementedError.prototype);
520
532
  }
521
- return {
522
- stat,
523
- access,
524
- constants,
525
- readFile,
526
- writeFile,
527
- readdir,
528
- mkdir,
529
- };
530
533
  }
531
- /**
532
- * Note: [๐ŸŸข] Code in this file should never be never released in packages that could be imported into browser environment
533
- */
534
534
 
535
535
  /**
536
536
  * Make error report URL for the given error
@@ -601,120 +601,566 @@ class UnexpectedError extends Error {
601
601
  }
602
602
 
603
603
  /**
604
- * Orders JSON object by keys
604
+ * @@@
605
605
  *
606
- * @returns The same type of object as the input re-ordered
607
- * @public exported from `@promptbook/utils`
606
+ * Note: `$` is used to indicate that this function is not a pure function - it access global scope
607
+ *
608
+ * @private internal function of `$Register`
608
609
  */
609
- function orderJson(options) {
610
- const { value, order } = options;
611
- const orderedValue = {
612
- ...(order === undefined ? {} : Object.fromEntries(order.map((key) => [key, undefined]))),
613
- ...value,
614
- };
615
- return orderedValue;
610
+ function $getGlobalScope() {
611
+ return Function('return this')();
616
612
  }
617
613
 
618
614
  /**
619
- * Freezes the given object and all its nested objects recursively
620
- *
621
- * Note: `$` is used to indicate that this function is not a pure function - it mutates given object
622
- * Note: This function mutates the object and returns the original (but mutated-deep-freezed) object
615
+ * @@@
623
616
  *
624
- * @returns The same object as the input, but deeply frozen
617
+ * @param text @@@
618
+ * @returns @@@
619
+ * @example 'HELLO_WORLD'
620
+ * @example 'I_LOVE_PROMPTBOOK'
625
621
  * @public exported from `@promptbook/utils`
626
622
  */
627
- function $deepFreeze(objectValue) {
628
- if (Array.isArray(objectValue)) {
629
- return Object.freeze(objectValue.map((item) => $deepFreeze(item)));
630
- }
631
- const propertyNames = Object.getOwnPropertyNames(objectValue);
632
- for (const propertyName of propertyNames) {
633
- const value = objectValue[propertyName];
634
- if (value && typeof value === 'object') {
635
- $deepFreeze(value);
623
+ function normalizeTo_SCREAMING_CASE(text) {
624
+ let charType;
625
+ let lastCharType = 'OTHER';
626
+ let normalizedName = '';
627
+ for (const char of text) {
628
+ let normalizedChar;
629
+ if (/^[a-z]$/.test(char)) {
630
+ charType = 'LOWERCASE';
631
+ normalizedChar = char.toUpperCase();
632
+ }
633
+ else if (/^[A-Z]$/.test(char)) {
634
+ charType = 'UPPERCASE';
635
+ normalizedChar = char;
636
+ }
637
+ else if (/^[0-9]$/.test(char)) {
638
+ charType = 'NUMBER';
639
+ normalizedChar = char;
640
+ }
641
+ else {
642
+ charType = 'OTHER';
643
+ normalizedChar = '_';
644
+ }
645
+ if (charType !== lastCharType &&
646
+ !(lastCharType === 'UPPERCASE' && charType === 'LOWERCASE') &&
647
+ !(lastCharType === 'NUMBER') &&
648
+ !(charType === 'NUMBER')) {
649
+ normalizedName += '_';
636
650
  }
651
+ normalizedName += normalizedChar;
652
+ lastCharType = charType;
637
653
  }
638
- Object.freeze(objectValue);
639
- return objectValue;
654
+ normalizedName = normalizedName.replace(/_+/g, '_');
655
+ normalizedName = normalizedName.replace(/_?\/_?/g, '/');
656
+ normalizedName = normalizedName.replace(/^_/, '');
657
+ normalizedName = normalizedName.replace(/_$/, '');
658
+ return normalizedName;
640
659
  }
641
660
  /**
642
- * TODO: [๐Ÿง ] Is there a way how to meaningfully test this utility
661
+ * TODO: Tests
662
+ * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: 'Moje tabule' })).toEqual('/VtG7sR9rRJqwNEdM2/Moje tabule');
663
+ * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: 'ฤ›ลกฤล™ลพลพรฝรกรญรบลฏ' })).toEqual('/VtG7sR9rRJqwNEdM2/escrzyaieuu');
664
+ * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: ' ahoj ' })).toEqual('/VtG7sR9rRJqwNEdM2/ahoj');
665
+ * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: ' ahoj_ahojAhoj ahoj ' })).toEqual('/VtG7sR9rRJqwNEdM2/ahoj-ahoj-ahoj-ahoj');
666
+ * TODO: [๐ŸŒบ] Use some intermediate util splitWords
643
667
  */
644
668
 
645
669
  /**
646
- * Checks if the value is [๐Ÿš‰] serializable as JSON
647
- * If not, throws an UnexpectedError with a rich error message and tracking
648
- *
649
- * - Almost all primitives are serializable BUT:
650
- * - `undefined` is not serializable
651
- * - `NaN` is not serializable
652
- * - Objects and arrays are serializable if all their properties are serializable
653
- * - Functions are not serializable
654
- * - Circular references are not serializable
655
- * - `Date` objects are not serializable
656
- * - `Map` and `Set` objects are not serializable
657
- * - `RegExp` objects are not serializable
658
- * - `Error` objects are not serializable
659
- * - `Symbol` objects are not serializable
660
- * - And much more...
670
+ * @@@
661
671
  *
662
- * @throws UnexpectedError if the value is not serializable as JSON
672
+ * @param text @@@
673
+ * @returns @@@
674
+ * @example 'hello_world'
675
+ * @example 'i_love_promptbook'
663
676
  * @public exported from `@promptbook/utils`
664
677
  */
665
- function checkSerializableAsJson(options) {
666
- const { value, name, message } = options;
667
- if (value === undefined) {
668
- throw new UnexpectedError(`${name} is undefined`);
669
- }
670
- else if (value === null) {
671
- return;
672
- }
673
- else if (typeof value === 'boolean') {
674
- return;
675
- }
676
- else if (typeof value === 'number' && !isNaN(value)) {
677
- return;
678
- }
679
- else if (typeof value === 'string') {
680
- return;
681
- }
682
- else if (typeof value === 'symbol') {
683
- throw new UnexpectedError(`${name} is symbol`);
684
- }
685
- else if (typeof value === 'function') {
686
- throw new UnexpectedError(`${name} is function`);
687
- }
688
- else if (typeof value === 'object' && Array.isArray(value)) {
689
- for (let i = 0; i < value.length; i++) {
690
- checkSerializableAsJson({ name: `${name}[${i}]`, value: value[i], message });
691
- }
692
- }
693
- else if (typeof value === 'object') {
694
- if (value instanceof Date) {
695
- throw new UnexpectedError(spaceTrim((block) => `
696
- \`${name}\` is Date
697
-
698
- Use \`string_date_iso8601\` instead
678
+ function normalizeTo_snake_case(text) {
679
+ return normalizeTo_SCREAMING_CASE(text).toLowerCase();
680
+ }
699
681
 
700
- Additional message for \`${name}\`:
701
- ${block(message || '(nothing)')}
702
- `));
682
+ /**
683
+ * Register is @@@
684
+ *
685
+ * Note: `$` is used to indicate that this function is not a pure function - it accesses and adds variables in global scope.
686
+ *
687
+ * @private internal utility, exported are only signleton instances of this class
688
+ */
689
+ class $Register {
690
+ constructor(registerName) {
691
+ this.registerName = registerName;
692
+ const storageName = `_promptbook_${normalizeTo_snake_case(registerName)}`;
693
+ const globalScope = $getGlobalScope();
694
+ if (globalScope[storageName] === undefined) {
695
+ globalScope[storageName] = [];
703
696
  }
704
- else if (value instanceof Map) {
705
- throw new UnexpectedError(`${name} is Map`);
697
+ else if (!Array.isArray(globalScope[storageName])) {
698
+ throw new UnexpectedError(`Expected (global) ${storageName} to be an array, but got ${typeof globalScope[storageName]}`);
706
699
  }
707
- else if (value instanceof Set) {
708
- throw new UnexpectedError(`${name} is Set`);
700
+ this.storage = globalScope[storageName];
701
+ }
702
+ list() {
703
+ // <- TODO: ReadonlyDeep<ReadonlyArray<TRegistered>>
704
+ return this.storage;
705
+ }
706
+ register(registered) {
707
+ const { packageName, className } = registered;
708
+ const existingRegistrationIndex = this.storage.findIndex((item) => item.packageName === packageName && item.className === className);
709
+ const existingRegistration = this.storage[existingRegistrationIndex];
710
+ if (!existingRegistration) {
711
+ this.storage.push(registered);
709
712
  }
710
- else if (value instanceof RegExp) {
711
- throw new UnexpectedError(`${name} is RegExp`);
713
+ else {
714
+ this.storage[existingRegistrationIndex] = registered;
712
715
  }
713
- else if (value instanceof Error) {
714
- throw new UnexpectedError(spaceTrim((block) => `
715
- \`${name}\` is unserialized Error
716
-
717
- Use function \`serializeError\`
716
+ return {
717
+ registerName: this.registerName,
718
+ packageName,
719
+ className,
720
+ get isDestroyed() {
721
+ return false;
722
+ },
723
+ destroy() {
724
+ throw new NotYetImplementedError(`Registration to ${this.registerName} is permanent in this version of Promptbook`);
725
+ },
726
+ };
727
+ }
728
+ }
729
+
730
+ /**
731
+ * @@@
732
+ *
733
+ * Note: `$` is used to indicate that this interacts with the global scope
734
+ * @singleton Only one instance of each register is created per build, but thare can be more @@@
735
+ * @public exported from `@promptbook/core`
736
+ */
737
+ const $llmToolsMetadataRegister = new $Register('llm_tools_metadata');
738
+ /**
739
+ * TODO: [ยฎ] DRY Register logic
740
+ */
741
+
742
+ /**
743
+ * @@@
744
+ *
745
+ * Note: `$` is used to indicate that this interacts with the global scope
746
+ * @singleton Only one instance of each register is created per build, but thare can be more @@@
747
+ * @public exported from `@promptbook/core`
748
+ */
749
+ const $llmToolsRegister = new $Register('llm_execution_tools_constructors');
750
+ /**
751
+ * TODO: [ยฎ] DRY Register logic
752
+ */
753
+
754
+ /**
755
+ * Path to the `.env` file which was used to configure LLM tools
756
+ *
757
+ * Note: `$` is used to indicate that this variable is changed by side effect in `$provideLlmToolsConfigurationFromEnv` through `$setUsedEnvFilename`
758
+ */
759
+ let $usedEnvFilename = null;
760
+ /**
761
+ * Pass the `.env` file which was used to configure LLM tools
762
+ *
763
+ * Note: `$` is used to indicate that this variable is making side effect
764
+ *
765
+ * @private internal log of `$provideLlmToolsConfigurationFromEnv` and `$registeredLlmToolsMessage`
766
+ */
767
+ function $setUsedEnvFilename(filepath) {
768
+ $usedEnvFilename = filepath;
769
+ }
770
+ /**
771
+ * Creates a message with all registered LLM tools
772
+ *
773
+ * Note: This function is used to create a (error) message when there is no constructor for some LLM provider
774
+ *
775
+ * @private internal function of `createLlmToolsFromConfiguration` and `$provideLlmToolsFromEnv`
776
+ */
777
+ function $registeredLlmToolsMessage() {
778
+ let env;
779
+ if ($isRunningInNode()) {
780
+ env = process.env;
781
+ // <- TODO: [โš›] Some DRY way how to get to `process.env` and pass it into functions - ACRY search for `env`
782
+ }
783
+ else {
784
+ env = {};
785
+ }
786
+ /**
787
+ * Mixes registered LLM tools from $llmToolsMetadataRegister and $llmToolsRegister
788
+ */
789
+ const all = [];
790
+ for (const { title, packageName, className, envVariables } of $llmToolsMetadataRegister.list()) {
791
+ if (all.some((item) => item.packageName === packageName && item.className === className)) {
792
+ continue;
793
+ }
794
+ all.push({ title, packageName, className, envVariables });
795
+ }
796
+ for (const { packageName, className } of $llmToolsRegister.list()) {
797
+ if (all.some((item) => item.packageName === packageName && item.className === className)) {
798
+ continue;
799
+ }
800
+ all.push({ packageName, className });
801
+ }
802
+ const metadata = all.map((metadata) => {
803
+ var _a, _b;
804
+ const isMetadataAviailable = $llmToolsMetadataRegister
805
+ .list()
806
+ .find(({ packageName, className }) => metadata.packageName === packageName && metadata.className === className);
807
+ const isInstalled = $llmToolsRegister
808
+ .list()
809
+ .find(({ packageName, className }) => metadata.packageName === packageName && metadata.className === className);
810
+ const isFullyConfigured = ((_a = metadata.envVariables) === null || _a === void 0 ? void 0 : _a.every((envVariableName) => env[envVariableName] !== undefined)) || false;
811
+ const isPartiallyConfigured = ((_b = metadata.envVariables) === null || _b === void 0 ? void 0 : _b.some((envVariableName) => env[envVariableName] !== undefined)) || false;
812
+ // <- Note: [๐Ÿ—จ]
813
+ return { ...metadata, isMetadataAviailable, isInstalled, isFullyConfigured, isPartiallyConfigured };
814
+ });
815
+ const usedEnvMessage = $usedEnvFilename === null ? `Unknown \`.env\` file` : `Used \`.env\` file:\n${$usedEnvFilename}`;
816
+ if (metadata.length === 0) {
817
+ return spaceTrim((block) => `
818
+ No LLM providers are available.
819
+
820
+ ${block(usedEnvMessage)}
821
+ `);
822
+ }
823
+ return spaceTrim((block) => `
824
+
825
+ ${block(usedEnvMessage)}
826
+
827
+ Relevant environment variables:
828
+ ${block(Object.keys(env)
829
+ .filter((envVariableName) => metadata.some(({ envVariables }) => envVariables === null || envVariables === void 0 ? void 0 : envVariables.includes(envVariableName)))
830
+ .map((envVariableName) => `- \`${envVariableName}\``)
831
+ .join('\n'))}
832
+
833
+ Available LLM providers are:
834
+ ${block(metadata
835
+ .map(({ title, packageName, className, envVariables, isMetadataAviailable, isInstalled, isFullyConfigured, isPartiallyConfigured, }, i) => {
836
+ const morePieces = [];
837
+ if (just(false)) ;
838
+ else if (!isMetadataAviailable && !isInstalled) {
839
+ // TODO: [๏ฟฝ][๏ฟฝ] Maybe do allow to do auto-install if package not registered and not found
840
+ morePieces.push(`Not installed and no metadata, looks like a unexpected behavior`);
841
+ }
842
+ else if (isMetadataAviailable && !isInstalled) {
843
+ // TODO: [๏ฟฝ][๏ฟฝ]
844
+ morePieces.push(`Not installed`);
845
+ }
846
+ else if (!isMetadataAviailable && isInstalled) {
847
+ morePieces.push(`No metadata but installed, looks like a unexpected behavior`);
848
+ }
849
+ else if (isMetadataAviailable && isInstalled) {
850
+ morePieces.push(`Installed`);
851
+ }
852
+ else {
853
+ morePieces.push(`unknown state, looks like a unexpected behavior`);
854
+ } /* not else */
855
+ if (isFullyConfigured) {
856
+ morePieces.push(`Configured`);
857
+ }
858
+ else if (isPartiallyConfigured) {
859
+ morePieces.push(`Partially confugured, missing ${envVariables === null || envVariables === void 0 ? void 0 : envVariables.filter((envVariable) => env[envVariable] === undefined).join(' + ')}`);
860
+ }
861
+ else {
862
+ if (envVariables !== null) {
863
+ morePieces.push(`Not configured, to configure set env ${envVariables === null || envVariables === void 0 ? void 0 : envVariables.join(' + ')}`);
864
+ }
865
+ else {
866
+ morePieces.push(`Not configured`); // <- Note: Can not be configured via environment variables
867
+ }
868
+ }
869
+ let providerMessage = spaceTrim(`
870
+ ${i + 1}) **${title}** \`${className}\` from \`${packageName}\`
871
+ ${morePieces.join('; ')}
872
+ `);
873
+ if ($isRunningInNode) {
874
+ if (isInstalled && isFullyConfigured) {
875
+ providerMessage = colors.green(providerMessage);
876
+ }
877
+ else if (isInstalled && isPartiallyConfigured) {
878
+ providerMessage = colors.yellow(providerMessage);
879
+ }
880
+ else {
881
+ providerMessage = colors.gray(providerMessage);
882
+ }
883
+ }
884
+ return providerMessage;
885
+ })
886
+ .join('\n'))}
887
+ `);
888
+ }
889
+ /**
890
+ * TODO: [ยฎ] DRY Register logic
891
+ * TODO: [๐Ÿง ][โš›] Maybe pass env as argument
892
+ */
893
+
894
+ /**
895
+ * Just says that the variable is not used but should be kept
896
+ * No side effects.
897
+ *
898
+ * Note: It can be usefull for:
899
+ *
900
+ * 1) Suppressing eager optimization of unused imports
901
+ * 2) Suppressing eslint errors of unused variables in the tests
902
+ * 3) Keeping the type of the variable for type testing
903
+ *
904
+ * @param value any values
905
+ * @returns void
906
+ * @private within the repository
907
+ */
908
+ function keepUnused(...valuesToKeep) {
909
+ }
910
+
911
+ /**
912
+ * Just says that the variable is not used directlys but should be kept because the existence of the variable is important
913
+ *
914
+ * @param value any values
915
+ * @returns void
916
+ * @private within the repository
917
+ */
918
+ function $sideEffect(...sideEffectSubjects) {
919
+ keepUnused(...sideEffectSubjects);
920
+ }
921
+
922
+ /**
923
+ * Checks if value is valid email
924
+ *
925
+ * @public exported from `@promptbook/utils`
926
+ */
927
+ function isValidEmail(email) {
928
+ if (typeof email !== 'string') {
929
+ return false;
930
+ }
931
+ if (email.split('\n').length > 1) {
932
+ return false;
933
+ }
934
+ return /^.+@.+\..+$/.test(email);
935
+ }
936
+
937
+ /**
938
+ * Tests if given string is valid URL.
939
+ *
940
+ * Note: Dataurl are considered perfectly valid.
941
+ * Note: There are two simmilar functions:
942
+ * - `isValidUrl` which tests any URL
943
+ * - `isValidPipelineUrl` *(this one)* which tests just promptbook URL
944
+ *
945
+ * @public exported from `@promptbook/utils`
946
+ */
947
+ function isValidUrl(url) {
948
+ if (typeof url !== 'string') {
949
+ return false;
950
+ }
951
+ try {
952
+ if (url.startsWith('blob:')) {
953
+ url = url.replace(/^blob:/, '');
954
+ }
955
+ const urlObject = new URL(url /* because fail is handled */);
956
+ if (!['http:', 'https:', 'data:'].includes(urlObject.protocol)) {
957
+ return false;
958
+ }
959
+ return true;
960
+ }
961
+ catch (error) {
962
+ return false;
963
+ }
964
+ }
965
+
966
+ /**
967
+ * Stores data in memory (HEAP)
968
+ *
969
+ * @public exported from `@promptbook/core`
970
+ */
971
+ class MemoryStorage {
972
+ constructor() {
973
+ this.storage = {};
974
+ }
975
+ /**
976
+ * Returns the number of key/value pairs currently present in the list associated with the object.
977
+ */
978
+ get length() {
979
+ return Object.keys(this.storage).length;
980
+ }
981
+ /**
982
+ * Empties the list associated with the object of all key/value pairs, if there are any.
983
+ */
984
+ clear() {
985
+ this.storage = {};
986
+ }
987
+ /**
988
+ * 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.
989
+ */
990
+ getItem(key) {
991
+ return this.storage[key] || null;
992
+ }
993
+ /**
994
+ * 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.
995
+ */
996
+ key(index) {
997
+ return Object.keys(this.storage)[index] || null;
998
+ }
999
+ /**
1000
+ * Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.
1001
+ */
1002
+ setItem(key, value) {
1003
+ this.storage[key] = value;
1004
+ }
1005
+ /**
1006
+ * 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.
1007
+ */
1008
+ removeItem(key) {
1009
+ delete this.storage[key];
1010
+ }
1011
+ }
1012
+
1013
+ /**
1014
+ * Just marks a place of place where should be something implemented
1015
+ * No side effects.
1016
+ *
1017
+ * Note: It can be usefull suppressing eslint errors of unused variables
1018
+ *
1019
+ * @param value any values
1020
+ * @returns void
1021
+ * @private within the repository
1022
+ */
1023
+ function TODO_USE(...value) {
1024
+ }
1025
+
1026
+ /**
1027
+ * @@@
1028
+ *
1029
+ * @public exported from `@promptbook/node`
1030
+ */
1031
+ function $provideFilesystemForNode(options) {
1032
+ if (!$isRunningInNode()) {
1033
+ throw new EnvironmentMismatchError('Function `$provideFilesystemForNode` works only in Node.js environment');
1034
+ }
1035
+ return {
1036
+ stat,
1037
+ access,
1038
+ constants,
1039
+ readFile,
1040
+ writeFile,
1041
+ readdir,
1042
+ mkdir,
1043
+ };
1044
+ }
1045
+ /**
1046
+ * Note: [๐ŸŸข] Code in this file should never be never released in packages that could be imported into browser environment
1047
+ */
1048
+
1049
+ /**
1050
+ * Orders JSON object by keys
1051
+ *
1052
+ * @returns The same type of object as the input re-ordered
1053
+ * @public exported from `@promptbook/utils`
1054
+ */
1055
+ function orderJson(options) {
1056
+ const { value, order } = options;
1057
+ const orderedValue = {
1058
+ ...(order === undefined ? {} : Object.fromEntries(order.map((key) => [key, undefined]))),
1059
+ ...value,
1060
+ };
1061
+ return orderedValue;
1062
+ }
1063
+
1064
+ /**
1065
+ * Freezes the given object and all its nested objects recursively
1066
+ *
1067
+ * Note: `$` is used to indicate that this function is not a pure function - it mutates given object
1068
+ * Note: This function mutates the object and returns the original (but mutated-deep-freezed) object
1069
+ *
1070
+ * @returns The same object as the input, but deeply frozen
1071
+ * @public exported from `@promptbook/utils`
1072
+ */
1073
+ function $deepFreeze(objectValue) {
1074
+ if (Array.isArray(objectValue)) {
1075
+ return Object.freeze(objectValue.map((item) => $deepFreeze(item)));
1076
+ }
1077
+ const propertyNames = Object.getOwnPropertyNames(objectValue);
1078
+ for (const propertyName of propertyNames) {
1079
+ const value = objectValue[propertyName];
1080
+ if (value && typeof value === 'object') {
1081
+ $deepFreeze(value);
1082
+ }
1083
+ }
1084
+ Object.freeze(objectValue);
1085
+ return objectValue;
1086
+ }
1087
+ /**
1088
+ * TODO: [๐Ÿง ] Is there a way how to meaningfully test this utility
1089
+ */
1090
+
1091
+ /**
1092
+ * Checks if the value is [๐Ÿš‰] serializable as JSON
1093
+ * If not, throws an UnexpectedError with a rich error message and tracking
1094
+ *
1095
+ * - Almost all primitives are serializable BUT:
1096
+ * - `undefined` is not serializable
1097
+ * - `NaN` is not serializable
1098
+ * - Objects and arrays are serializable if all their properties are serializable
1099
+ * - Functions are not serializable
1100
+ * - Circular references are not serializable
1101
+ * - `Date` objects are not serializable
1102
+ * - `Map` and `Set` objects are not serializable
1103
+ * - `RegExp` objects are not serializable
1104
+ * - `Error` objects are not serializable
1105
+ * - `Symbol` objects are not serializable
1106
+ * - And much more...
1107
+ *
1108
+ * @throws UnexpectedError if the value is not serializable as JSON
1109
+ * @public exported from `@promptbook/utils`
1110
+ */
1111
+ function checkSerializableAsJson(options) {
1112
+ const { value, name, message } = options;
1113
+ if (value === undefined) {
1114
+ throw new UnexpectedError(`${name} is undefined`);
1115
+ }
1116
+ else if (value === null) {
1117
+ return;
1118
+ }
1119
+ else if (typeof value === 'boolean') {
1120
+ return;
1121
+ }
1122
+ else if (typeof value === 'number' && !isNaN(value)) {
1123
+ return;
1124
+ }
1125
+ else if (typeof value === 'string') {
1126
+ return;
1127
+ }
1128
+ else if (typeof value === 'symbol') {
1129
+ throw new UnexpectedError(`${name} is symbol`);
1130
+ }
1131
+ else if (typeof value === 'function') {
1132
+ throw new UnexpectedError(`${name} is function`);
1133
+ }
1134
+ else if (typeof value === 'object' && Array.isArray(value)) {
1135
+ for (let i = 0; i < value.length; i++) {
1136
+ checkSerializableAsJson({ name: `${name}[${i}]`, value: value[i], message });
1137
+ }
1138
+ }
1139
+ else if (typeof value === 'object') {
1140
+ if (value instanceof Date) {
1141
+ throw new UnexpectedError(spaceTrim((block) => `
1142
+ \`${name}\` is Date
1143
+
1144
+ Use \`string_date_iso8601\` instead
1145
+
1146
+ Additional message for \`${name}\`:
1147
+ ${block(message || '(nothing)')}
1148
+ `));
1149
+ }
1150
+ else if (value instanceof Map) {
1151
+ throw new UnexpectedError(`${name} is Map`);
1152
+ }
1153
+ else if (value instanceof Set) {
1154
+ throw new UnexpectedError(`${name} is Set`);
1155
+ }
1156
+ else if (value instanceof RegExp) {
1157
+ throw new UnexpectedError(`${name} is RegExp`);
1158
+ }
1159
+ else if (value instanceof Error) {
1160
+ throw new UnexpectedError(spaceTrim((block) => `
1161
+ \`${name}\` is unserialized Error
1162
+
1163
+ Use function \`serializeError\`
718
1164
 
719
1165
  Additional message for \`${name}\`:
720
1166
  ${block(message || '(nothing)')}
@@ -1058,35 +1504,6 @@ function isValidFilePath(filename) {
1058
1504
  * TODO: [๐Ÿ] Implement for MacOs
1059
1505
  */
1060
1506
 
1061
- /**
1062
- * Tests if given string is valid URL.
1063
- *
1064
- * Note: Dataurl are considered perfectly valid.
1065
- * Note: There are two simmilar functions:
1066
- * - `isValidUrl` which tests any URL
1067
- * - `isValidPipelineUrl` *(this one)* which tests just promptbook URL
1068
- *
1069
- * @public exported from `@promptbook/utils`
1070
- */
1071
- function isValidUrl(url) {
1072
- if (typeof url !== 'string') {
1073
- return false;
1074
- }
1075
- try {
1076
- if (url.startsWith('blob:')) {
1077
- url = url.replace(/^blob:/, '');
1078
- }
1079
- const urlObject = new URL(url /* because fail is handled */);
1080
- if (!['http:', 'https:', 'data:'].includes(urlObject.protocol)) {
1081
- return false;
1082
- }
1083
- return true;
1084
- }
1085
- catch (error) {
1086
- return false;
1087
- }
1088
- }
1089
-
1090
1507
  const defaultDiacriticsRemovalMap = [
1091
1508
  {
1092
1509
  base: 'A',
@@ -1470,32 +1887,177 @@ class FileCacheStorage {
1470
1887
  // TODO: [๐ŸŒ—]
1471
1888
  return value;
1472
1889
  }
1473
- /**
1474
- * @@@ Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.
1475
- */
1476
- async setItem(key, value) {
1477
- const filename = this.getFilenameForKey(key);
1478
- if (!isSerializableAsJson(value)) {
1479
- throw new UnexpectedError(`The "${key}" you want to store in JSON file is not serializable as JSON`);
1480
- }
1481
- const fileContent = stringifyPipelineJson(value);
1482
- await mkdir(dirname(filename), { recursive: true }); // <- [0]
1483
- await writeFile(filename, fileContent, 'utf-8');
1890
+ /**
1891
+ * @@@ Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.
1892
+ */
1893
+ async setItem(key, value) {
1894
+ const filename = this.getFilenameForKey(key);
1895
+ if (!isSerializableAsJson(value)) {
1896
+ throw new UnexpectedError(`The "${key}" you want to store in JSON file is not serializable as JSON`);
1897
+ }
1898
+ const fileContent = stringifyPipelineJson(value);
1899
+ await mkdir(dirname(filename), { recursive: true }); // <- [0]
1900
+ await writeFile(filename, fileContent, 'utf-8');
1901
+ }
1902
+ /**
1903
+ * @@@ 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.
1904
+ */
1905
+ async removeItem(key) {
1906
+ const filename = this.getFilenameForKey(key);
1907
+ // TODO: [๐Ÿง ] What to use `unlink` or `rm`
1908
+ await unlink(filename);
1909
+ // <- TODO: [๐Ÿฟ][๐Ÿง ] Maybe remove empty folders
1910
+ // [0] When `setItem` and `removeItem` called, the state of the file system should be the same
1911
+ }
1912
+ }
1913
+ /**
1914
+ * TODO: [๐ŸŒ—] Maybe some checkers, not all valid JSONs are desired and valid values
1915
+ * Note: [๐ŸŸข] Code in this file should never be never released in packages that could be imported into browser environment
1916
+ */
1917
+
1918
+ /**
1919
+ * This error indicates problems parsing the format value
1920
+ *
1921
+ * For example, when the format value is not a valid JSON or CSV
1922
+ * This is not thrown directly but in extended classes
1923
+ *
1924
+ * @public exported from `@promptbook/core`
1925
+ */
1926
+ class AbstractFormatError extends Error {
1927
+ // Note: To allow instanceof do not put here error `name`
1928
+ // public readonly name = 'AbstractFormatError';
1929
+ constructor(message) {
1930
+ super(message);
1931
+ Object.setPrototypeOf(this, AbstractFormatError.prototype);
1932
+ }
1933
+ }
1934
+
1935
+ /**
1936
+ * This error indicates problem with parsing of CSV
1937
+ *
1938
+ * @public exported from `@promptbook/core`
1939
+ */
1940
+ class CsvFormatError extends AbstractFormatError {
1941
+ constructor(message) {
1942
+ super(message);
1943
+ this.name = 'CsvFormatError';
1944
+ Object.setPrototypeOf(this, CsvFormatError.prototype);
1945
+ }
1946
+ }
1947
+
1948
+ /**
1949
+ * AuthenticationError is thrown from login function which is dependency of remote server
1950
+ *
1951
+ * @public exported from `@promptbook/core`
1952
+ */
1953
+ class AuthenticationError extends Error {
1954
+ constructor(message) {
1955
+ super(message);
1956
+ this.name = 'AuthenticationError';
1957
+ Object.setPrototypeOf(this, AuthenticationError.prototype);
1958
+ }
1959
+ }
1960
+
1961
+ /**
1962
+ * This error indicates that the pipeline collection cannot be propperly loaded
1963
+ *
1964
+ * @public exported from `@promptbook/core`
1965
+ */
1966
+ class CollectionError extends Error {
1967
+ constructor(message) {
1968
+ super(message);
1969
+ this.name = 'CollectionError';
1970
+ Object.setPrototypeOf(this, CollectionError.prototype);
1971
+ }
1972
+ }
1973
+
1974
+ /**
1975
+ * This error occurs when some expectation is not met in the execution of the pipeline
1976
+ *
1977
+ * @public exported from `@promptbook/core`
1978
+ * Note: Do not throw this error, its reserved for `checkExpectations` and `createPipelineExecutor` and public ONLY to be serializable through remote server
1979
+ * Note: Always thrown in `checkExpectations` and catched in `createPipelineExecutor` and rethrown as `PipelineExecutionError`
1980
+ * Note: This is a kindof subtype of PipelineExecutionError
1981
+ */
1982
+ class ExpectError extends Error {
1983
+ constructor(message) {
1984
+ super(message);
1985
+ this.name = 'ExpectError';
1986
+ Object.setPrototypeOf(this, ExpectError.prototype);
1987
+ }
1988
+ }
1989
+
1990
+ /**
1991
+ * This error indicates that the promptbook can not retrieve knowledge from external sources
1992
+ *
1993
+ * @public exported from `@promptbook/core`
1994
+ */
1995
+ class KnowledgeScrapeError extends Error {
1996
+ constructor(message) {
1997
+ super(message);
1998
+ this.name = 'KnowledgeScrapeError';
1999
+ Object.setPrototypeOf(this, KnowledgeScrapeError.prototype);
2000
+ }
2001
+ }
2002
+
2003
+ /**
2004
+ * This error type indicates that some limit was reached
2005
+ *
2006
+ * @public exported from `@promptbook/core`
2007
+ */
2008
+ class LimitReachedError extends Error {
2009
+ constructor(message) {
2010
+ super(message);
2011
+ this.name = 'LimitReachedError';
2012
+ Object.setPrototypeOf(this, LimitReachedError.prototype);
2013
+ }
2014
+ }
2015
+
2016
+ /**
2017
+ * This error type indicates that some tools are missing for pipeline execution or preparation
2018
+ *
2019
+ * @public exported from `@promptbook/core`
2020
+ */
2021
+ class MissingToolsError extends Error {
2022
+ constructor(message) {
2023
+ super(spaceTrim$1((block) => `
2024
+ ${block(message)}
2025
+
2026
+ Note: You have probbably forgot to provide some tools for pipeline execution or preparation
2027
+
2028
+ `));
2029
+ this.name = 'MissingToolsError';
2030
+ Object.setPrototypeOf(this, MissingToolsError.prototype);
2031
+ }
2032
+ }
2033
+
2034
+ /**
2035
+ * This error indicates that promptbook not found in the collection
2036
+ *
2037
+ * @public exported from `@promptbook/core`
2038
+ */
2039
+ class NotFoundError extends Error {
2040
+ constructor(message) {
2041
+ super(message);
2042
+ this.name = 'NotFoundError';
2043
+ Object.setPrototypeOf(this, NotFoundError.prototype);
1484
2044
  }
1485
- /**
1486
- * @@@ 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.
1487
- */
1488
- async removeItem(key) {
1489
- const filename = this.getFilenameForKey(key);
1490
- // TODO: [๐Ÿง ] What to use `unlink` or `rm`
1491
- await unlink(filename);
1492
- // <- TODO: [๐Ÿฟ][๐Ÿง ] Maybe remove empty folders
1493
- // [0] When `setItem` and `removeItem` called, the state of the file system should be the same
2045
+ }
2046
+
2047
+ /**
2048
+ * This error indicates that the promptbook in a markdown format cannot be parsed into a valid promptbook object
2049
+ *
2050
+ * @public exported from `@promptbook/core`
2051
+ */
2052
+ class ParseError extends Error {
2053
+ constructor(message) {
2054
+ super(message);
2055
+ this.name = 'ParseError';
2056
+ Object.setPrototypeOf(this, ParseError.prototype);
1494
2057
  }
1495
2058
  }
1496
2059
  /**
1497
- * TODO: [๐ŸŒ—] Maybe some checkers, not all valid JSONs are desired and valid values
1498
- * Note: [๐ŸŸข] Code in this file should never be never released in packages that could be imported into browser environment
2060
+ * TODO: Maybe split `ParseError` and `ApplyError`
1499
2061
  */
1500
2062
 
1501
2063
  /**
@@ -1533,51 +2095,260 @@ class PipelineExecutionError extends Error {
1533
2095
  */
1534
2096
 
1535
2097
  /**
1536
- * Stores data in memory (HEAP)
2098
+ * This error indicates that the promptbook object has valid syntax (=can be parsed) but contains logical errors (like circular dependencies)
1537
2099
  *
1538
2100
  * @public exported from `@promptbook/core`
1539
2101
  */
1540
- class MemoryStorage {
1541
- constructor() {
1542
- this.storage = {};
2102
+ class PipelineLogicError extends Error {
2103
+ constructor(message) {
2104
+ super(message);
2105
+ this.name = 'PipelineLogicError';
2106
+ Object.setPrototypeOf(this, PipelineLogicError.prototype);
2107
+ }
2108
+ }
2109
+
2110
+ /**
2111
+ * This error indicates errors in referencing promptbooks between each other
2112
+ *
2113
+ * @public exported from `@promptbook/core`
2114
+ */
2115
+ class PipelineUrlError extends Error {
2116
+ constructor(message) {
2117
+ super(message);
2118
+ this.name = 'PipelineUrlError';
2119
+ Object.setPrototypeOf(this, PipelineUrlError.prototype);
2120
+ }
2121
+ }
2122
+
2123
+ /**
2124
+ * Index of all custom errors
2125
+ *
2126
+ * @public exported from `@promptbook/core`
2127
+ */
2128
+ const PROMPTBOOK_ERRORS = {
2129
+ AbstractFormatError,
2130
+ CsvFormatError,
2131
+ CollectionError,
2132
+ EnvironmentMismatchError,
2133
+ ExpectError,
2134
+ KnowledgeScrapeError,
2135
+ LimitReachedError,
2136
+ MissingToolsError,
2137
+ NotFoundError,
2138
+ NotYetImplementedError,
2139
+ ParseError,
2140
+ PipelineExecutionError,
2141
+ PipelineLogicError,
2142
+ PipelineUrlError,
2143
+ UnexpectedError,
2144
+ // TODO: [๐Ÿช‘]> VersionMismatchError,
2145
+ };
2146
+ /**
2147
+ * Index of all javascript errors
2148
+ *
2149
+ * @private for internal usage
2150
+ */
2151
+ const COMMON_JAVASCRIPT_ERRORS = {
2152
+ Error,
2153
+ EvalError,
2154
+ RangeError,
2155
+ ReferenceError,
2156
+ SyntaxError,
2157
+ TypeError,
2158
+ URIError,
2159
+ AggregateError,
2160
+ AuthenticationError,
2161
+ /*
2162
+ Note: Not widely supported
2163
+ > InternalError,
2164
+ > ModuleError,
2165
+ > HeapError,
2166
+ > WebAssemblyCompileError,
2167
+ > WebAssemblyRuntimeError,
2168
+ */
2169
+ };
2170
+ /**
2171
+ * Index of all errors
2172
+ *
2173
+ * @private for internal usage
2174
+ */
2175
+ const ALL_ERRORS = {
2176
+ ...PROMPTBOOK_ERRORS,
2177
+ ...COMMON_JAVASCRIPT_ERRORS,
2178
+ };
2179
+ /**
2180
+ * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
2181
+ */
2182
+
2183
+ /**
2184
+ * Deserializes the error object
2185
+ *
2186
+ * @public exported from `@promptbook/utils`
2187
+ */
2188
+ function deserializeError(error) {
2189
+ const { name, stack, id } = error; // Added id
2190
+ let { message } = error;
2191
+ let ErrorClass = ALL_ERRORS[error.name];
2192
+ if (ErrorClass === undefined) {
2193
+ ErrorClass = Error;
2194
+ message = `${name}: ${message}`;
2195
+ }
2196
+ if (stack !== undefined && stack !== '') {
2197
+ message = spaceTrim((block) => `
2198
+ ${block(message)}
2199
+
2200
+ Original stack trace:
2201
+ ${block(stack || '')}
2202
+ `);
2203
+ }
2204
+ const deserializedError = new ErrorClass(message);
2205
+ deserializedError.id = id; // Assign id to the error object
2206
+ return deserializedError;
2207
+ }
2208
+
2209
+ /**
2210
+ * Creates a connection to the remote proxy server.
2211
+ *
2212
+ * Note: This function creates a connection to the remote server and returns a socket but responsibility of closing the connection is on the caller
2213
+ *
2214
+ * @private internal utility function
2215
+ */
2216
+ async function createRemoteClient(options) {
2217
+ const { remoteServerUrl } = options;
2218
+ let path = new URL(remoteServerUrl).pathname;
2219
+ if (path.endsWith('/')) {
2220
+ path = path.slice(0, -1);
2221
+ }
2222
+ path = `${path}/socket.io`;
2223
+ return new Promise((resolve, reject) => {
2224
+ const socket = io(remoteServerUrl, {
2225
+ retries: CONNECTION_RETRIES_LIMIT,
2226
+ timeout: CONNECTION_TIMEOUT_MS,
2227
+ path,
2228
+ transports: [/*'websocket', <- TODO: [๐ŸŒฌ] Make websocket transport work */ 'polling'],
2229
+ });
2230
+ // console.log('Connecting to', this.options.remoteServerUrl.href, { socket });
2231
+ socket.on('connect', () => {
2232
+ resolve(socket);
2233
+ });
2234
+ // TODO: [๐Ÿ’ฉ] Better timeout handling
2235
+ setTimeout(() => {
2236
+ reject(new Error(`Timeout while connecting to ${remoteServerUrl}`));
2237
+ }, CONNECTION_TIMEOUT_MS);
2238
+ });
2239
+ }
2240
+
2241
+ /**
2242
+ * Remote server is a proxy server that uses its execution tools internally and exposes the executor interface externally.
2243
+ *
2244
+ * You can simply use `RemoteExecutionTools` on client-side javascript and connect to your remote server.
2245
+ * This is useful to make all logic on browser side but not expose your API keys or no need to use customer's GPU.
2246
+ *
2247
+ * @see https://github.com/webgptorg/promptbook#remote-server
2248
+ * @public exported from `@promptbook/remote-client`
2249
+ */
2250
+ class RemoteLlmExecutionTools {
2251
+ /* <- TODO: [๐Ÿš] `, Destroyable` */
2252
+ constructor(options) {
2253
+ this.options = options;
2254
+ }
2255
+ get title() {
2256
+ // TODO: [๐Ÿง ] Maybe fetch title+description from the remote server (as well as if model methods are defined)
2257
+ return 'Remote server';
2258
+ }
2259
+ get description() {
2260
+ return 'Use all models by your remote server';
1543
2261
  }
1544
2262
  /**
1545
- * Returns the number of key/value pairs currently present in the list associated with the object.
2263
+ * Check the configuration of all execution tools
1546
2264
  */
1547
- get length() {
1548
- return Object.keys(this.storage).length;
2265
+ async checkConfiguration() {
2266
+ const socket = await createRemoteClient(this.options);
2267
+ socket.disconnect();
2268
+ // TODO: [main] !!3 Check version of the remote server and compatibility
2269
+ // TODO: [๐ŸŽ] Send checkConfiguration
1549
2270
  }
1550
2271
  /**
1551
- * Empties the list associated with the object of all key/value pairs, if there are any.
2272
+ * List all available models that can be used
1552
2273
  */
1553
- clear() {
1554
- this.storage = {};
2274
+ async listModels() {
2275
+ // TODO: [๐Ÿ‘’] Listing models (and checking configuration) probbably should go through REST API not Socket.io
2276
+ const socket = await createRemoteClient(this.options);
2277
+ socket.emit('listModels-request', {
2278
+ identification: this.options.identification,
2279
+ } /* <- Note: [๐Ÿค›] */);
2280
+ const promptResult = await new Promise((resolve, reject) => {
2281
+ socket.on('listModels-response', (response) => {
2282
+ resolve(response.models);
2283
+ socket.disconnect();
2284
+ });
2285
+ socket.on('error', (error) => {
2286
+ reject(deserializeError(error));
2287
+ socket.disconnect();
2288
+ });
2289
+ });
2290
+ socket.disconnect();
2291
+ return promptResult;
1555
2292
  }
1556
2293
  /**
1557
- * 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.
2294
+ * Calls remote proxy server to use a chat model
1558
2295
  */
1559
- getItem(key) {
1560
- return this.storage[key] || null;
2296
+ callChatModel(prompt) {
2297
+ if (this.options.isVerbose) {
2298
+ console.info(`๐Ÿ–‹ Remote callChatModel call`);
2299
+ }
2300
+ return /* not await */ this.callCommonModel(prompt);
1561
2301
  }
1562
2302
  /**
1563
- * 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.
2303
+ * Calls remote proxy server to use a completion model
1564
2304
  */
1565
- key(index) {
1566
- return Object.keys(this.storage)[index] || null;
2305
+ callCompletionModel(prompt) {
2306
+ if (this.options.isVerbose) {
2307
+ console.info(`๐Ÿ’ฌ Remote callCompletionModel call`);
2308
+ }
2309
+ return /* not await */ this.callCommonModel(prompt);
1567
2310
  }
1568
2311
  /**
1569
- * Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.
2312
+ * Calls remote proxy server to use a embedding model
1570
2313
  */
1571
- setItem(key, value) {
1572
- this.storage[key] = value;
2314
+ callEmbeddingModel(prompt) {
2315
+ if (this.options.isVerbose) {
2316
+ console.info(`๐Ÿ’ฌ Remote callEmbeddingModel call`);
2317
+ }
2318
+ return /* not await */ this.callCommonModel(prompt);
1573
2319
  }
2320
+ // <- Note: [๐Ÿค–] callXxxModel
1574
2321
  /**
1575
- * 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.
2322
+ * Calls remote proxy server to use both completion or chat model
1576
2323
  */
1577
- removeItem(key) {
1578
- delete this.storage[key];
2324
+ async callCommonModel(prompt) {
2325
+ const socket = await createRemoteClient(this.options);
2326
+ socket.emit('prompt-request', {
2327
+ identification: this.options.identification,
2328
+ prompt,
2329
+ } /* <- Note: [๐Ÿค›] */);
2330
+ const promptResult = await new Promise((resolve, reject) => {
2331
+ socket.on('prompt-response', (response) => {
2332
+ resolve(response.promptResult);
2333
+ socket.disconnect();
2334
+ });
2335
+ socket.on('error', (error) => {
2336
+ reject(deserializeError(error));
2337
+ socket.disconnect();
2338
+ });
2339
+ });
2340
+ socket.disconnect();
2341
+ return promptResult;
1579
2342
  }
1580
2343
  }
2344
+ /**
2345
+ * TODO: Maybe use `$exportJson`
2346
+ * TODO: [๐Ÿง ][๐Ÿ›] Maybe not `isAnonymous: boolean` BUT `mode: 'ANONYMOUS'|'COLLECTION'`
2347
+ * TODO: [๐Ÿ“] Allow to list compatible models with each variant
2348
+ * TODO: [๐Ÿ—ฏ] RemoteLlmExecutionTools should extend Destroyable and implement IDestroyable
2349
+ * TODO: [๐Ÿง ][๐ŸŒฐ] Allow to pass `title` for tracking purposes
2350
+ * TODO: [๐Ÿง ] Maybe remove `@promptbook/remote-client` and just use `@promptbook/core`
2351
+ */
1581
2352
 
1582
2353
  /**
1583
2354
  * Simple wrapper `new Date().toISOString()`
@@ -1863,339 +2634,25 @@ function countUsage(llmTools) {
1863
2634
  * TODO: [๐Ÿง ][๐ŸŒฏ] Maybe a way how to hide ability to `get totalUsage`
1864
2635
  * > const [llmToolsWithUsage,getUsage] = countTotalUsage(llmTools);
1865
2636
  * TODO: [๐Ÿ‘ทโ€โ™‚๏ธ] @@@ Manual about construction of llmTools
1866
- */
1867
-
1868
- /**
1869
- * This error type indicates that some part of the code is not implemented yet
1870
- *
1871
- * @public exported from `@promptbook/core`
1872
- */
1873
- class NotYetImplementedError extends Error {
1874
- constructor(message) {
1875
- super(spaceTrim$1((block) => `
1876
- ${block(message)}
1877
-
1878
- Note: This feature is not implemented yet but it will be soon.
1879
-
1880
- If you want speed up the implementation or just read more, look here:
1881
- https://github.com/webgptorg/promptbook
1882
-
1883
- Or contact us on pavol@ptbk.io
1884
-
1885
- `));
1886
- this.name = 'NotYetImplementedError';
1887
- Object.setPrototypeOf(this, NotYetImplementedError.prototype);
1888
- }
1889
- }
1890
-
1891
- /**
1892
- * @@@
1893
- *
1894
- * Note: `$` is used to indicate that this function is not a pure function - it access global scope
1895
- *
1896
- * @private internal function of `$Register`
1897
- */
1898
- function $getGlobalScope() {
1899
- return Function('return this')();
1900
- }
1901
-
1902
- /**
1903
- * @@@
1904
- *
1905
- * @param text @@@
1906
- * @returns @@@
1907
- * @example 'HELLO_WORLD'
1908
- * @example 'I_LOVE_PROMPTBOOK'
1909
- * @public exported from `@promptbook/utils`
1910
- */
1911
- function normalizeTo_SCREAMING_CASE(text) {
1912
- let charType;
1913
- let lastCharType = 'OTHER';
1914
- let normalizedName = '';
1915
- for (const char of text) {
1916
- let normalizedChar;
1917
- if (/^[a-z]$/.test(char)) {
1918
- charType = 'LOWERCASE';
1919
- normalizedChar = char.toUpperCase();
1920
- }
1921
- else if (/^[A-Z]$/.test(char)) {
1922
- charType = 'UPPERCASE';
1923
- normalizedChar = char;
1924
- }
1925
- else if (/^[0-9]$/.test(char)) {
1926
- charType = 'NUMBER';
1927
- normalizedChar = char;
1928
- }
1929
- else {
1930
- charType = 'OTHER';
1931
- normalizedChar = '_';
1932
- }
1933
- if (charType !== lastCharType &&
1934
- !(lastCharType === 'UPPERCASE' && charType === 'LOWERCASE') &&
1935
- !(lastCharType === 'NUMBER') &&
1936
- !(charType === 'NUMBER')) {
1937
- normalizedName += '_';
1938
- }
1939
- normalizedName += normalizedChar;
1940
- lastCharType = charType;
1941
- }
1942
- normalizedName = normalizedName.replace(/_+/g, '_');
1943
- normalizedName = normalizedName.replace(/_?\/_?/g, '/');
1944
- normalizedName = normalizedName.replace(/^_/, '');
1945
- normalizedName = normalizedName.replace(/_$/, '');
1946
- return normalizedName;
1947
- }
1948
- /**
1949
- * TODO: Tests
1950
- * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: 'Moje tabule' })).toEqual('/VtG7sR9rRJqwNEdM2/Moje tabule');
1951
- * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: 'ฤ›ลกฤล™ลพลพรฝรกรญรบลฏ' })).toEqual('/VtG7sR9rRJqwNEdM2/escrzyaieuu');
1952
- * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: ' ahoj ' })).toEqual('/VtG7sR9rRJqwNEdM2/ahoj');
1953
- * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: ' ahoj_ahojAhoj ahoj ' })).toEqual('/VtG7sR9rRJqwNEdM2/ahoj-ahoj-ahoj-ahoj');
1954
- * TODO: [๐ŸŒบ] Use some intermediate util splitWords
1955
- */
1956
-
1957
- /**
1958
- * @@@
1959
- *
1960
- * @param text @@@
1961
- * @returns @@@
1962
- * @example 'hello_world'
1963
- * @example 'i_love_promptbook'
1964
- * @public exported from `@promptbook/utils`
1965
- */
1966
- function normalizeTo_snake_case(text) {
1967
- return normalizeTo_SCREAMING_CASE(text).toLowerCase();
1968
- }
1969
-
1970
- /**
1971
- * Register is @@@
1972
- *
1973
- * Note: `$` is used to indicate that this function is not a pure function - it accesses and adds variables in global scope.
1974
- *
1975
- * @private internal utility, exported are only signleton instances of this class
1976
- */
1977
- class $Register {
1978
- constructor(registerName) {
1979
- this.registerName = registerName;
1980
- const storageName = `_promptbook_${normalizeTo_snake_case(registerName)}`;
1981
- const globalScope = $getGlobalScope();
1982
- if (globalScope[storageName] === undefined) {
1983
- globalScope[storageName] = [];
1984
- }
1985
- else if (!Array.isArray(globalScope[storageName])) {
1986
- throw new UnexpectedError(`Expected (global) ${storageName} to be an array, but got ${typeof globalScope[storageName]}`);
1987
- }
1988
- this.storage = globalScope[storageName];
1989
- }
1990
- list() {
1991
- // <- TODO: ReadonlyDeep<ReadonlyArray<TRegistered>>
1992
- return this.storage;
1993
- }
1994
- register(registered) {
1995
- const { packageName, className } = registered;
1996
- const existingRegistrationIndex = this.storage.findIndex((item) => item.packageName === packageName && item.className === className);
1997
- const existingRegistration = this.storage[existingRegistrationIndex];
1998
- if (!existingRegistration) {
1999
- this.storage.push(registered);
2000
- }
2001
- else {
2002
- this.storage[existingRegistrationIndex] = registered;
2003
- }
2004
- return {
2005
- registerName: this.registerName,
2006
- packageName,
2007
- className,
2008
- get isDestroyed() {
2009
- return false;
2010
- },
2011
- destroy() {
2012
- throw new NotYetImplementedError(`Registration to ${this.registerName} is permanent in this version of Promptbook`);
2013
- },
2014
- };
2015
- }
2016
- }
2017
-
2018
- /**
2019
- * @@@
2020
- *
2021
- * Note: `$` is used to indicate that this interacts with the global scope
2022
- * @singleton Only one instance of each register is created per build, but thare can be more @@@
2023
- * @public exported from `@promptbook/core`
2024
- */
2025
- const $llmToolsMetadataRegister = new $Register('llm_tools_metadata');
2026
- /**
2027
- * TODO: [ยฎ] DRY Register logic
2028
- */
2029
-
2030
- /**
2031
- * Determines if the given path is a root path.
2032
- *
2033
- * Note: This does not check if the file exists only if the path is valid
2034
- * @public exported from `@promptbook/utils`
2035
- */
2036
- function isRootPath(value) {
2037
- if (value === '/') {
2038
- return true;
2039
- }
2040
- if (/^[A-Z]:\\$/i.test(value)) {
2041
- return true;
2042
- }
2043
- return false;
2044
- }
2045
- /**
2046
- * TODO: [๐Ÿ] Make for MacOS paths
2047
- */
2048
-
2049
- /**
2050
- * @@@
2051
- *
2052
- * Note: `$` is used to indicate that this interacts with the global scope
2053
- * @singleton Only one instance of each register is created per build, but thare can be more @@@
2054
- * @public exported from `@promptbook/core`
2055
- */
2056
- const $llmToolsRegister = new $Register('llm_execution_tools_constructors');
2057
- /**
2058
- * TODO: [ยฎ] DRY Register logic
2059
- */
2060
-
2061
- /**
2062
- * Path to the `.env` file which was used to configure LLM tools
2063
- *
2064
- * Note: `$` is used to indicate that this variable is changed by side effect in `$provideLlmToolsConfigurationFromEnv` through `$setUsedEnvFilename`
2065
- */
2066
- let $usedEnvFilename = null;
2067
- /**
2068
- * Pass the `.env` file which was used to configure LLM tools
2069
- *
2070
- * Note: `$` is used to indicate that this variable is making side effect
2071
- *
2072
- * @private internal log of `$provideLlmToolsConfigurationFromEnv` and `$registeredLlmToolsMessage`
2073
- */
2074
- function $setUsedEnvFilename(filepath) {
2075
- $usedEnvFilename = filepath;
2076
- }
2637
+ */
2638
+
2077
2639
  /**
2078
- * Creates a message with all registered LLM tools
2079
- *
2080
- * Note: This function is used to create a (error) message when there is no constructor for some LLM provider
2640
+ * Determines if the given path is a root path.
2081
2641
  *
2082
- * @private internal function of `createLlmToolsFromConfiguration` and `$provideLlmToolsFromEnv`
2642
+ * Note: This does not check if the file exists only if the path is valid
2643
+ * @public exported from `@promptbook/utils`
2083
2644
  */
2084
- function $registeredLlmToolsMessage() {
2085
- let env;
2086
- if ($isRunningInNode()) {
2087
- env = process.env;
2088
- // <- TODO: [โš›] Some DRY way how to get to `process.env` and pass it into functions - ACRY search for `env`
2089
- }
2090
- else {
2091
- env = {};
2092
- }
2093
- /**
2094
- * Mixes registered LLM tools from $llmToolsMetadataRegister and $llmToolsRegister
2095
- */
2096
- const all = [];
2097
- for (const { title, packageName, className, envVariables } of $llmToolsMetadataRegister.list()) {
2098
- if (all.some((item) => item.packageName === packageName && item.className === className)) {
2099
- continue;
2100
- }
2101
- all.push({ title, packageName, className, envVariables });
2102
- }
2103
- for (const { packageName, className } of $llmToolsRegister.list()) {
2104
- if (all.some((item) => item.packageName === packageName && item.className === className)) {
2105
- continue;
2106
- }
2107
- all.push({ packageName, className });
2645
+ function isRootPath(value) {
2646
+ if (value === '/') {
2647
+ return true;
2108
2648
  }
2109
- const metadata = all.map((metadata) => {
2110
- var _a, _b;
2111
- const isMetadataAviailable = $llmToolsMetadataRegister
2112
- .list()
2113
- .find(({ packageName, className }) => metadata.packageName === packageName && metadata.className === className);
2114
- const isInstalled = $llmToolsRegister
2115
- .list()
2116
- .find(({ packageName, className }) => metadata.packageName === packageName && metadata.className === className);
2117
- const isFullyConfigured = ((_a = metadata.envVariables) === null || _a === void 0 ? void 0 : _a.every((envVariableName) => env[envVariableName] !== undefined)) || false;
2118
- const isPartiallyConfigured = ((_b = metadata.envVariables) === null || _b === void 0 ? void 0 : _b.some((envVariableName) => env[envVariableName] !== undefined)) || false;
2119
- // <- Note: [๐Ÿ—จ]
2120
- return { ...metadata, isMetadataAviailable, isInstalled, isFullyConfigured, isPartiallyConfigured };
2121
- });
2122
- const usedEnvMessage = $usedEnvFilename === null ? `Unknown \`.env\` file` : `Used \`.env\` file:\n${$usedEnvFilename}`;
2123
- if (metadata.length === 0) {
2124
- return spaceTrim((block) => `
2125
- No LLM providers are available.
2126
-
2127
- ${block(usedEnvMessage)}
2128
- `);
2649
+ if (/^[A-Z]:\\$/i.test(value)) {
2650
+ return true;
2129
2651
  }
2130
- return spaceTrim((block) => `
2131
-
2132
- ${block(usedEnvMessage)}
2133
-
2134
- Relevant environment variables:
2135
- ${block(Object.keys(env)
2136
- .filter((envVariableName) => metadata.some(({ envVariables }) => envVariables === null || envVariables === void 0 ? void 0 : envVariables.includes(envVariableName)))
2137
- .map((envVariableName) => `- \`${envVariableName}\``)
2138
- .join('\n'))}
2139
-
2140
- Available LLM providers are:
2141
- ${block(metadata
2142
- .map(({ title, packageName, className, envVariables, isMetadataAviailable, isInstalled, isFullyConfigured, isPartiallyConfigured, }, i) => {
2143
- const morePieces = [];
2144
- if (just(false)) ;
2145
- else if (!isMetadataAviailable && !isInstalled) {
2146
- // TODO: [๏ฟฝ][๏ฟฝ] Maybe do allow to do auto-install if package not registered and not found
2147
- morePieces.push(`Not installed and no metadata, looks like a unexpected behavior`);
2148
- }
2149
- else if (isMetadataAviailable && !isInstalled) {
2150
- // TODO: [๏ฟฝ][๏ฟฝ]
2151
- morePieces.push(`Not installed`);
2152
- }
2153
- else if (!isMetadataAviailable && isInstalled) {
2154
- morePieces.push(`No metadata but installed, looks like a unexpected behavior`);
2155
- }
2156
- else if (isMetadataAviailable && isInstalled) {
2157
- morePieces.push(`Installed`);
2158
- }
2159
- else {
2160
- morePieces.push(`unknown state, looks like a unexpected behavior`);
2161
- } /* not else */
2162
- if (isFullyConfigured) {
2163
- morePieces.push(`Configured`);
2164
- }
2165
- else if (isPartiallyConfigured) {
2166
- morePieces.push(`Partially confugured, missing ${envVariables === null || envVariables === void 0 ? void 0 : envVariables.filter((envVariable) => env[envVariable] === undefined).join(' + ')}`);
2167
- }
2168
- else {
2169
- if (envVariables !== null) {
2170
- morePieces.push(`Not configured, to configure set env ${envVariables === null || envVariables === void 0 ? void 0 : envVariables.join(' + ')}`);
2171
- }
2172
- else {
2173
- morePieces.push(`Not configured`); // <- Note: Can not be configured via environment variables
2174
- }
2175
- }
2176
- let providerMessage = spaceTrim(`
2177
- ${i + 1}) **${title}** \`${className}\` from \`${packageName}\`
2178
- ${morePieces.join('; ')}
2179
- `);
2180
- if ($isRunningInNode) {
2181
- if (isInstalled && isFullyConfigured) {
2182
- providerMessage = colors.green(providerMessage);
2183
- }
2184
- else if (isInstalled && isPartiallyConfigured) {
2185
- providerMessage = colors.yellow(providerMessage);
2186
- }
2187
- else {
2188
- providerMessage = colors.gray(providerMessage);
2189
- }
2190
- }
2191
- return providerMessage;
2192
- })
2193
- .join('\n'))}
2194
- `);
2652
+ return false;
2195
2653
  }
2196
2654
  /**
2197
- * TODO: [ยฎ] DRY Register logic
2198
- * TODO: [๐Ÿง ][โš›] Maybe pass env as argument
2655
+ * TODO: [๐Ÿ] Make for MacOS paths
2199
2656
  */
2200
2657
 
2201
2658
  /**
@@ -2558,10 +3015,33 @@ async function $provideLlmToolsForWizzardOrCli(options) {
2558
3015
  if (!$isRunningInNode()) {
2559
3016
  throw new EnvironmentMismatchError('Function `$provideLlmToolsForWizzardOrCli` works only in Node.js environment');
2560
3017
  }
2561
- const { isCacheReloaded } = options !== null && options !== void 0 ? options : {};
3018
+ options = options !== null && options !== void 0 ? options : { strategy: 'BRING_YOUR_OWN_KEYS' };
3019
+ const { strategy, isCacheReloaded } = options;
3020
+ let llmExecutionTools;
3021
+ if (strategy === 'REMOTE_SERVER') {
3022
+ const { remoteServerUrl = DEFAULT_REMOTE_SERVER_URL, loginPrompt } = options;
3023
+ const storage = new MemoryStorage(); // <- TODO: !!!!!! Save to `.promptbook` folder
3024
+ const key = `${remoteServerUrl}-identification`;
3025
+ let identification = await storage.getItem(key);
3026
+ if (identification === null) {
3027
+ identification = await loginPrompt();
3028
+ await storage.setItem(key, identification);
3029
+ }
3030
+ llmExecutionTools = new RemoteLlmExecutionTools({
3031
+ remoteServerUrl,
3032
+ identification,
3033
+ });
3034
+ }
3035
+ else if (strategy === 'BRING_YOUR_OWN_KEYS') {
3036
+ llmExecutionTools = await $provideLlmToolsFromEnv();
3037
+ }
3038
+ else {
3039
+ throw new UnexpectedError(`\`$provideLlmToolsForWizzardOrCli\` wrong strategy "${strategy}"`);
3040
+ }
2562
3041
  return cacheLlmTools(countUsage(
3042
+ // <- TODO: [๐ŸŒฏ] We dont use countUsage at all, maybe just unwrap it
2563
3043
  // <- Note: for example here we don`t want the [๐ŸŒฏ]
2564
- await $provideLlmToolsFromEnv()), {
3044
+ llmExecutionTools), {
2565
3045
  storage: new FileCacheStorage({ fs: $provideFilesystemForNode() }, {
2566
3046
  rootFolderPath: join(process.cwd(), DEFAULT_EXECUTION_CACHE_DIRNAME),
2567
3047
  }),
@@ -2577,31 +3057,88 @@ async function $provideLlmToolsForWizzardOrCli(options) {
2577
3057
  */
2578
3058
 
2579
3059
  /**
2580
- * Just says that the variable is not used but should be kept
2581
- * No side effects.
2582
- *
2583
- * Note: It can be usefull for:
2584
- *
2585
- * 1) Suppressing eager optimization of unused imports
2586
- * 2) Suppressing eslint errors of unused variables in the tests
2587
- * 3) Keeping the type of the variable for type testing
2588
- *
2589
- * @param value any values
2590
- * @returns void
2591
- * @private within the repository
2592
- */
2593
- function keepUnused(...valuesToKeep) {
2594
- }
2595
-
2596
- /**
2597
- * Just says that the variable is not used directlys but should be kept because the existence of the variable is important
2598
- *
2599
- * @param value any values
2600
- * @returns void
2601
- * @private within the repository
3060
+ * @private utility of CLI
2602
3061
  */
2603
- function $sideEffect(...sideEffectSubjects) {
2604
- keepUnused(...sideEffectSubjects);
3062
+ function $provideLlmToolsForCli(options) {
3063
+ const { cliOptions: {
3064
+ /* TODO: Use verbose: isVerbose, */ interactive: isInteractive, provider, remoteServerUrl: remoteServerUrlRaw, }, } = options;
3065
+ let strategy;
3066
+ if (/^b/i.test(provider)) {
3067
+ strategy = 'BRING_YOUR_OWN_KEYS';
3068
+ }
3069
+ else if (/^r/i.test(provider)) {
3070
+ strategy = 'REMOTE_SERVER';
3071
+ }
3072
+ else {
3073
+ console.log(colors.red(`Unknown provider: "${provider}", please use "BRING_YOUR_OWN_KEYS" or "REMOTE_SERVER"`));
3074
+ process.exit(1);
3075
+ }
3076
+ if (strategy === 'BRING_YOUR_OWN_KEYS') {
3077
+ return /* not await */ $provideLlmToolsForWizzardOrCli({ strategy, ...options });
3078
+ }
3079
+ else if (strategy === 'REMOTE_SERVER') {
3080
+ if (!isValidUrl(remoteServerUrlRaw)) {
3081
+ console.log(colors.red(`Invalid URL of remote server: "${remoteServerUrlRaw}"`));
3082
+ process.exit(1);
3083
+ }
3084
+ const remoteServerUrl = remoteServerUrlRaw.endsWith('/') ? remoteServerUrlRaw.slice(0, -1) : remoteServerUrlRaw;
3085
+ return /* not await */ $provideLlmToolsForWizzardOrCli({
3086
+ strategy,
3087
+ appId: CLI_APP_ID,
3088
+ remoteServerUrl,
3089
+ ...options,
3090
+ async loginPrompt() {
3091
+ if (!isInteractive) {
3092
+ console.log(colors.red(`You can not login to remote server in non-interactive mode`));
3093
+ process.exit(1);
3094
+ }
3095
+ const { username, password } = await prompts([
3096
+ {
3097
+ type: 'text',
3098
+ name: 'username',
3099
+ message: 'Enter your email:',
3100
+ validate: (value) => (isValidEmail(value) ? true : 'Valid email is required'),
3101
+ },
3102
+ {
3103
+ type: 'password',
3104
+ name: 'password',
3105
+ message: 'Enter your password:',
3106
+ validate: (value) => value.length /* <- TODO: [๐Ÿง ] Better password validation */ > 0
3107
+ ? true
3108
+ : 'Password is required',
3109
+ },
3110
+ ]);
3111
+ const loginUrl = `${remoteServerUrl}/login`;
3112
+ const response = await fetch(loginUrl, {
3113
+ method: 'POST',
3114
+ headers: {
3115
+ 'Content-Type': 'application/json',
3116
+ },
3117
+ body: JSON.stringify({
3118
+ appId: CLI_APP_ID,
3119
+ username,
3120
+ password,
3121
+ }),
3122
+ });
3123
+ console.log('!!!', {
3124
+ loginUrl,
3125
+ username,
3126
+ password,
3127
+ // type: response.type,
3128
+ // text: await response.text(),
3129
+ });
3130
+ const body = (await response.json());
3131
+ if ('error' in body) {
3132
+ console.log(colors.red(body.error.message));
3133
+ process.exit(1);
3134
+ }
3135
+ return body.identification;
3136
+ },
3137
+ });
3138
+ }
3139
+ else {
3140
+ throw new UnexpectedError(`\`$provideLlmToolsForCli\` wrong strategy "${strategy}"`);
3141
+ }
2605
3142
  }
2606
3143
 
2607
3144
  /**
@@ -2618,8 +3155,10 @@ function $initializeListModelsCommand(program) {
2618
3155
  `));
2619
3156
  listModelsCommand.alias('models');
2620
3157
  listModelsCommand.alias('llm');
2621
- listModelsCommand.action(handleActionErrors(async () => {
2622
- const llm = await $provideLlmToolsForWizzardOrCli({});
3158
+ listModelsCommand.action(handleActionErrors(async (cliOptions) => {
3159
+ console.log('!!!', cliOptions);
3160
+ // TODO: !!!!!! Not relevant for remote server and also for `about` command
3161
+ const llm = await $provideLlmToolsForCli({ cliOptions });
2623
3162
  $sideEffect(llm);
2624
3163
  // <- Note: Providing LLM tools will make a side effect of registering all available LLM tools to show the message
2625
3164
  console.info($registeredLlmToolsMessage());
@@ -3179,49 +3718,73 @@ function $initializeListScrapersCommand(program) {
3179
3718
  */
3180
3719
 
3181
3720
  /**
3182
- * Converts PipelineCollection to serialized JSON
3721
+ * Initializes `login` command for Promptbook CLI utilities
3183
3722
  *
3184
- * Note: Functions `collectionToJson` and `createCollectionFromJson` are complementary
3723
+ * Note: `$` is used to indicate that this function is not a pure function - it registers a command in the CLI
3185
3724
  *
3186
- * @public exported from `@promptbook/core`
3187
- */
3188
- async function collectionToJson(collection) {
3189
- const pipelineUrls = await collection.listPipelines();
3190
- const promptbooks = await Promise.all(pipelineUrls.map((url) => collection.getPipelineByUrl(url)));
3191
- return promptbooks;
3192
- }
3193
- /**
3194
- * TODO: [๐Ÿง ] Maybe clear `sourceFile` or clear when exposing through API or remote server
3725
+ * @private internal function of `promptbookCli`
3195
3726
  */
3727
+ function $initializeLoginCommand(program) {
3728
+ const loginCommand = program.command('login');
3729
+ loginCommand.description(spaceTrim(`
3730
+ Login to the remote Promptbook server
3731
+ `));
3732
+ loginCommand.action(handleActionErrors(async () => {
3733
+ // @@@
3734
+ console.error(colors.green(spaceTrim(`
3735
+ You will be logged in to https://promptbook.studio server.
3736
+ If you don't have an account, it will be created automatically.
3737
+ `)));
3738
+ // !!!!!!!!! Remove from here and use $provideLlmToolsForCli
3739
+ const { email, password } = await prompts([
3740
+ {
3741
+ type: 'text',
3742
+ name: 'email',
3743
+ message: 'Enter your email:',
3744
+ validate: (value) => (isValidEmail(value) ? true : 'Valid email is required'),
3745
+ },
3746
+ {
3747
+ type: 'password',
3748
+ name: 'password',
3749
+ message: 'Enter your password:',
3750
+ validate: (value) => value.length /* <- TODO: [๐Ÿง ] Better password validation */ > 0 ? true : 'Password is required',
3751
+ },
3752
+ ]);
3753
+ TODO_USE(email, password);
3754
+ await forTime(1000);
3755
+ console.error(colors.green(spaceTrim(`
3756
+ Your account ${email} was successfully created.
3196
3757
 
3197
- /**
3198
- * This error indicates that the promptbook in a markdown format cannot be parsed into a valid promptbook object
3199
- *
3200
- * @public exported from `@promptbook/core`
3201
- */
3202
- class ParseError extends Error {
3203
- constructor(message) {
3204
- super(message);
3205
- this.name = 'ParseError';
3206
- Object.setPrototypeOf(this, ParseError.prototype);
3207
- }
3758
+ Please verify your email:
3759
+ https://brj.app/api/v1/customer/register-account?apiKey=PRODdh003eNKaec7PoO1AzU244tsL4WO
3760
+
3761
+ After verification, you will receive 500 000 credits for free ๐ŸŽ‰
3762
+ `)));
3763
+ return process.exit(0);
3764
+ }));
3208
3765
  }
3209
3766
  /**
3210
- * TODO: Maybe split `ParseError` and `ApplyError`
3767
+ * TODO: Pass remote server URL (and path)
3768
+ * TODO: Implement non-interactive login
3769
+ * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
3770
+ * Note: [๐ŸŸก] Code in this file should never be published outside of `@promptbook/cli`
3211
3771
  */
3212
3772
 
3213
3773
  /**
3214
- * This error indicates that the promptbook object has valid syntax (=can be parsed) but contains logical errors (like circular dependencies)
3774
+ * Converts PipelineCollection to serialized JSON
3775
+ *
3776
+ * Note: Functions `collectionToJson` and `createCollectionFromJson` are complementary
3215
3777
  *
3216
3778
  * @public exported from `@promptbook/core`
3217
3779
  */
3218
- class PipelineLogicError extends Error {
3219
- constructor(message) {
3220
- super(message);
3221
- this.name = 'PipelineLogicError';
3222
- Object.setPrototypeOf(this, PipelineLogicError.prototype);
3223
- }
3780
+ async function collectionToJson(collection) {
3781
+ const pipelineUrls = await collection.listPipelines();
3782
+ const promptbooks = await Promise.all(pipelineUrls.map((url) => collection.getPipelineByUrl(url)));
3783
+ return promptbooks;
3224
3784
  }
3785
+ /**
3786
+ * TODO: [๐Ÿง ] Maybe clear `sourceFile` or clear when exposing through API or remote server
3787
+ */
3225
3788
 
3226
3789
  /**
3227
3790
  * Tests if given string is valid semantic version
@@ -3619,21 +4182,6 @@ async function loadArchive(filePath, fs) {
3619
4182
 
3620
4183
  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"}];
3621
4184
 
3622
- /**
3623
- * Checks if value is valid email
3624
- *
3625
- * @public exported from `@promptbook/utils`
3626
- */
3627
- function isValidEmail(email) {
3628
- if (typeof email !== 'string') {
3629
- return false;
3630
- }
3631
- if (email.split('\n').length > 1) {
3632
- return false;
3633
- }
3634
- return /^.+@.+\..+$/.test(email);
3635
- }
3636
-
3637
4185
  /**
3638
4186
  * Function isValidJsonString will tell you if the string is valid JSON or not
3639
4187
  *
@@ -3867,32 +4415,6 @@ function taskParameterJsonToString(taskParameterJson) {
3867
4415
  * TODO: [๐Ÿง ] Should be in generated .book.md file GENERATOR_WARNING
3868
4416
  */
3869
4417
 
3870
- /**
3871
- * This error indicates that promptbook not found in the collection
3872
- *
3873
- * @public exported from `@promptbook/core`
3874
- */
3875
- class NotFoundError extends Error {
3876
- constructor(message) {
3877
- super(message);
3878
- this.name = 'NotFoundError';
3879
- Object.setPrototypeOf(this, NotFoundError.prototype);
3880
- }
3881
- }
3882
-
3883
- /**
3884
- * This error indicates errors in referencing promptbooks between each other
3885
- *
3886
- * @public exported from `@promptbook/core`
3887
- */
3888
- class PipelineUrlError extends Error {
3889
- constructor(message) {
3890
- super(message);
3891
- this.name = 'PipelineUrlError';
3892
- Object.setPrototypeOf(this, PipelineUrlError.prototype);
3893
- }
3894
- }
3895
-
3896
4418
  /**
3897
4419
  * Parses the task and returns the list of all parameter names
3898
4420
  *
@@ -4063,24 +4585,6 @@ function createCollectionFromJson(...promptbooks) {
4063
4585
  return new SimplePipelineCollection(...promptbooks);
4064
4586
  }
4065
4587
 
4066
- /**
4067
- * This error type indicates that some tools are missing for pipeline execution or preparation
4068
- *
4069
- * @public exported from `@promptbook/core`
4070
- */
4071
- class MissingToolsError extends Error {
4072
- constructor(message) {
4073
- super(spaceTrim$1((block) => `
4074
- ${block(message)}
4075
-
4076
- Note: You have probbably forgot to provide some tools for pipeline execution or preparation
4077
-
4078
- `));
4079
- this.name = 'MissingToolsError';
4080
- Object.setPrototypeOf(this, MissingToolsError.prototype);
4081
- }
4082
- }
4083
-
4084
4588
  /**
4085
4589
  * Determine if the pipeline is fully prepared
4086
4590
  *
@@ -4113,210 +4617,40 @@ function isPipelinePrepared(pipeline) {
4113
4617
  * TODO: [๐Ÿ ] Maybe base this on `makeValidator`
4114
4618
  * TODO: [๐ŸงŠ] Pipeline can be partially prepared, this should return true ONLY if fully prepared
4115
4619
  * TODO: [๐Ÿงฟ] Maybe do same process with same granularity and subfinctions as `preparePipeline`
4116
- * - [๐Ÿ] ? Is context in each task
4117
- * - [โ™จ] Are examples prepared
4118
- * - [โ™จ] Are tasks prepared
4119
- */
4120
-
4121
- /**
4122
- * Recursively converts JSON strings to JSON objects
4123
-
4124
- * @public exported from `@promptbook/utils`
4125
- */
4126
- function jsonStringsToJsons(object) {
4127
- if (object === null) {
4128
- return object;
4129
- }
4130
- if (Array.isArray(object)) {
4131
- return object.map(jsonStringsToJsons);
4132
- }
4133
- if (typeof object !== 'object') {
4134
- return object;
4135
- }
4136
- const newObject = { ...object };
4137
- for (const [key, value] of Object.entries(object)) {
4138
- if (typeof value === 'string' && isValidJsonString(value)) {
4139
- newObject[key] = JSON.parse(value);
4140
- }
4141
- else {
4142
- newObject[key] = jsonStringsToJsons(value);
4143
- }
4144
- }
4145
- return newObject;
4146
- }
4147
- /**
4148
- * TODO: Type the return type correctly
4149
- */
4150
-
4151
- /**
4152
- * This error indicates problems parsing the format value
4153
- *
4154
- * For example, when the format value is not a valid JSON or CSV
4155
- * This is not thrown directly but in extended classes
4156
- *
4157
- * @public exported from `@promptbook/core`
4158
- */
4159
- class AbstractFormatError extends Error {
4160
- // Note: To allow instanceof do not put here error `name`
4161
- // public readonly name = 'AbstractFormatError';
4162
- constructor(message) {
4163
- super(message);
4164
- Object.setPrototypeOf(this, AbstractFormatError.prototype);
4165
- }
4166
- }
4167
-
4168
- /**
4169
- * This error indicates problem with parsing of CSV
4170
- *
4171
- * @public exported from `@promptbook/core`
4172
- */
4173
- class CsvFormatError extends AbstractFormatError {
4174
- constructor(message) {
4175
- super(message);
4176
- this.name = 'CsvFormatError';
4177
- Object.setPrototypeOf(this, CsvFormatError.prototype);
4178
- }
4179
- }
4180
-
4181
- /**
4182
- * This error indicates that the pipeline collection cannot be propperly loaded
4183
- *
4184
- * @public exported from `@promptbook/core`
4185
- */
4186
- class CollectionError extends Error {
4187
- constructor(message) {
4188
- super(message);
4189
- this.name = 'CollectionError';
4190
- Object.setPrototypeOf(this, CollectionError.prototype);
4191
- }
4192
- }
4193
-
4194
- /**
4195
- * This error occurs when some expectation is not met in the execution of the pipeline
4196
- *
4197
- * @public exported from `@promptbook/core`
4198
- * Note: Do not throw this error, its reserved for `checkExpectations` and `createPipelineExecutor` and public ONLY to be serializable through remote server
4199
- * Note: Always thrown in `checkExpectations` and catched in `createPipelineExecutor` and rethrown as `PipelineExecutionError`
4200
- * Note: This is a kindof subtype of PipelineExecutionError
4201
- */
4202
- class ExpectError extends Error {
4203
- constructor(message) {
4204
- super(message);
4205
- this.name = 'ExpectError';
4206
- Object.setPrototypeOf(this, ExpectError.prototype);
4207
- }
4208
- }
4209
-
4210
- /**
4211
- * This error indicates that the promptbook can not retrieve knowledge from external sources
4212
- *
4213
- * @public exported from `@promptbook/core`
4214
- */
4215
- class KnowledgeScrapeError extends Error {
4216
- constructor(message) {
4217
- super(message);
4218
- this.name = 'KnowledgeScrapeError';
4219
- Object.setPrototypeOf(this, KnowledgeScrapeError.prototype);
4220
- }
4221
- }
4222
-
4223
- /**
4224
- * This error type indicates that some limit was reached
4225
- *
4226
- * @public exported from `@promptbook/core`
4227
- */
4228
- class LimitReachedError extends Error {
4229
- constructor(message) {
4230
- super(message);
4231
- this.name = 'LimitReachedError';
4232
- Object.setPrototypeOf(this, LimitReachedError.prototype);
4233
- }
4234
- }
4235
-
4236
- /**
4237
- * Index of all custom errors
4238
- *
4239
- * @public exported from `@promptbook/core`
4240
- */
4241
- const PROMPTBOOK_ERRORS = {
4242
- AbstractFormatError,
4243
- CsvFormatError,
4244
- CollectionError,
4245
- EnvironmentMismatchError,
4246
- ExpectError,
4247
- KnowledgeScrapeError,
4248
- LimitReachedError,
4249
- MissingToolsError,
4250
- NotFoundError,
4251
- NotYetImplementedError,
4252
- ParseError,
4253
- PipelineExecutionError,
4254
- PipelineLogicError,
4255
- PipelineUrlError,
4256
- UnexpectedError,
4257
- // TODO: [๐Ÿช‘]> VersionMismatchError,
4258
- };
4259
- /**
4260
- * Index of all javascript errors
4261
- *
4262
- * @private for internal usage
4263
- */
4264
- const COMMON_JAVASCRIPT_ERRORS = {
4265
- Error,
4266
- EvalError,
4267
- RangeError,
4268
- ReferenceError,
4269
- SyntaxError,
4270
- TypeError,
4271
- URIError,
4272
- AggregateError,
4273
- /*
4274
- Note: Not widely supported
4275
- > InternalError,
4276
- > ModuleError,
4277
- > HeapError,
4278
- > WebAssemblyCompileError,
4279
- > WebAssemblyRuntimeError,
4280
- */
4281
- };
4282
- /**
4283
- * Index of all errors
4284
- *
4285
- * @private for internal usage
4286
- */
4287
- const ALL_ERRORS = {
4288
- ...PROMPTBOOK_ERRORS,
4289
- ...COMMON_JAVASCRIPT_ERRORS,
4290
- };
4291
- /**
4292
- * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
4293
- */
4294
-
4295
- /**
4296
- * Deserializes the error object
4297
- *
4298
- * @public exported from `@promptbook/utils`
4299
- */
4300
- function deserializeError(error) {
4301
- const { name, stack, id } = error; // Added id
4302
- let { message } = error;
4303
- let ErrorClass = ALL_ERRORS[error.name];
4304
- if (ErrorClass === undefined) {
4305
- ErrorClass = Error;
4306
- message = `${name}: ${message}`;
4307
- }
4308
- if (stack !== undefined && stack !== '') {
4309
- message = spaceTrim((block) => `
4310
- ${block(message)}
4620
+ * - [๐Ÿ] ? Is context in each task
4621
+ * - [โ™จ] Are examples prepared
4622
+ * - [โ™จ] Are tasks prepared
4623
+ */
4311
4624
 
4312
- Original stack trace:
4313
- ${block(stack || '')}
4314
- `);
4625
+ /**
4626
+ * Recursively converts JSON strings to JSON objects
4627
+
4628
+ * @public exported from `@promptbook/utils`
4629
+ */
4630
+ function jsonStringsToJsons(object) {
4631
+ if (object === null) {
4632
+ return object;
4315
4633
  }
4316
- const deserializedError = new ErrorClass(message);
4317
- deserializedError.id = id; // Assign id to the error object
4318
- return deserializedError;
4634
+ if (Array.isArray(object)) {
4635
+ return object.map(jsonStringsToJsons);
4636
+ }
4637
+ if (typeof object !== 'object') {
4638
+ return object;
4639
+ }
4640
+ const newObject = { ...object };
4641
+ for (const [key, value] of Object.entries(object)) {
4642
+ if (typeof value === 'string' && isValidJsonString(value)) {
4643
+ newObject[key] = JSON.parse(value);
4644
+ }
4645
+ else {
4646
+ newObject[key] = jsonStringsToJsons(value);
4647
+ }
4648
+ }
4649
+ return newObject;
4319
4650
  }
4651
+ /**
4652
+ * TODO: Type the return type correctly
4653
+ */
4320
4654
 
4321
4655
  /**
4322
4656
  * Asserts that the execution of a Promptbook is successful
@@ -4469,6 +4803,10 @@ function serializeError(error) {
4469
4803
 
4470
4804
  Cannot serialize error with name "${name}"
4471
4805
 
4806
+ Authors of Promptbook probably forgot to add this error into the list of errors:
4807
+ https://github.com/webgptorg/promptbook/blob/main/src/errors/0-index.ts
4808
+
4809
+
4472
4810
  ${block(stack || message)}
4473
4811
 
4474
4812
  `));
@@ -11379,7 +11717,6 @@ function $initializeMakeCommand(program) {
11379
11717
  makeCommand.option('--no-validation', `Do not validate logic of pipelines in collection`, true);
11380
11718
  makeCommand.option('--validation', `Types of validations separated by comma (options "logic","imports")`, 'logic,imports');
11381
11719
  makeCommand.option('-r, --reload', `Call LLM models even if same prompt with result is in the cache`, false);
11382
- makeCommand.option('-v, --verbose', `Is output verbose`, false);
11383
11720
  makeCommand.option('-o, --output <path>', spaceTrim(`
11384
11721
  Where to save the builded collection
11385
11722
 
@@ -11393,7 +11730,8 @@ function $initializeMakeCommand(program) {
11393
11730
  Note: This can be used only with "javascript" or "typescript" format
11394
11731
 
11395
11732
  `), DEFAULT_GET_PIPELINE_COLLECTION_FUNCTION_NAME);
11396
- makeCommand.action(handleActionErrors(async (path, { projectName, rootUrl, format, functionName, validation, reload: isCacheReloaded, verbose: isVerbose, output, }) => {
11733
+ makeCommand.action(handleActionErrors(async (path, cliOptions) => {
11734
+ const { projectName, rootUrl, format, functionName, validation, reload: isCacheReloaded, verbose: isVerbose, output, } = cliOptions;
11397
11735
  if (!isValidJavascriptName(functionName)) {
11398
11736
  console.error(colors.red(`Function name "${functionName}" is not valid javascript name`));
11399
11737
  return process.exit(1);
@@ -11421,7 +11759,10 @@ function $initializeMakeCommand(program) {
11421
11759
  isCacheReloaded,
11422
11760
  }; /* <- TODO: ` satisfies PrepareAndScrapeOptions` */
11423
11761
  const fs = $provideFilesystemForNode(prepareAndScrapeOptions);
11424
- const llm = await $provideLlmToolsForWizzardOrCli(prepareAndScrapeOptions);
11762
+ const llm = await $provideLlmToolsForCli({
11763
+ cliOptions,
11764
+ ...prepareAndScrapeOptions,
11765
+ });
11425
11766
  const executables = await $provideExecutablesForNode(prepareAndScrapeOptions);
11426
11767
  const tools = {
11427
11768
  llm,
@@ -11689,8 +12030,8 @@ function $initializePrettifyCommand(program) {
11689
12030
  // <- TODO: [๐ŸงŸโ€โ™‚๏ธ] Unite path to promptbook collection argument
11690
12031
  'Pipelines to prettify as glob pattern');
11691
12032
  prettifyCommand.option('-i, --ignore <glob>', `Ignore as glob pattern`);
11692
- prettifyCommand.option('-v, --verbose', `Is output verbose`, false);
11693
- prettifyCommand.action(handleActionErrors(async (filesGlob, { ignore, verbose: isVerbose }) => {
12033
+ prettifyCommand.action(handleActionErrors(async (filesGlob, cliOptions) => {
12034
+ const { ignore, verbose: isVerbose } = cliOptions;
11694
12035
  const filenames = await glob(filesGlob, { ignore });
11695
12036
  // <- TODO: [๐Ÿ˜ถ]
11696
12037
  for (const filename of filenames) {
@@ -12291,13 +12632,12 @@ function $initializeRunCommand(program) {
12291
12632
  // TODO: [๐Ÿง…] DRY command arguments
12292
12633
  runCommand.argument('[pipelineSource]', 'Path to book file OR URL to book file, if not provided it will be asked');
12293
12634
  runCommand.option('-r, --reload', `Call LLM models even if same prompt with result is in the cache`, false);
12294
- runCommand.option('-v, --verbose', `Is output verbose`, false);
12295
- runCommand.option('--no-interactive', `Input is not interactive, if true you need to pass all the input parameters through --json`);
12296
12635
  runCommand.option('--no-formfactor', `When set, behavior of the interactive mode is not changed by the formfactor of the pipeline`);
12297
12636
  runCommand.option('-j, --json <json>', `Pass all or some input parameters as JSON record, if used the output is also returned as JSON`);
12298
12637
  runCommand.option('-s, --save-report <path>', `Save report to file`);
12299
- runCommand.action(handleActionErrors(async (pipelineSource, options) => {
12300
- const { reload: isCacheReloaded, interactive: isInteractive, formfactor: isFormfactorUsed, json, verbose: isVerbose, saveReport, } = options;
12638
+ runCommand.action(handleActionErrors(async (pipelineSource, cliOptions) => {
12639
+ console.log('!!!', cliOptions);
12640
+ const { reload: isCacheReloaded, interactive: isInteractive, formfactor: isFormfactorUsed, json, verbose: isVerbose, saveReport, } = cliOptions;
12301
12641
  if (pipelineSource.includes('-') && normalizeToKebabCase(pipelineSource) === pipelineSource) {
12302
12642
  console.error(colors.red(`""${pipelineSource}" is not a valid command or book. See 'ptbk --help'.`));
12303
12643
  return process.exit(1);
@@ -12322,7 +12662,7 @@ function $initializeRunCommand(program) {
12322
12662
  const fs = $provideFilesystemForNode(prepareAndScrapeOptions);
12323
12663
  let llm;
12324
12664
  try {
12325
- llm = await $provideLlmToolsForWizzardOrCli(prepareAndScrapeOptions);
12665
+ llm = await $provideLlmToolsForCli({ cliOptions, ...prepareAndScrapeOptions });
12326
12666
  }
12327
12667
  catch (error) {
12328
12668
  if (!(error instanceof Error)) {
@@ -12379,7 +12719,7 @@ function $initializeRunCommand(program) {
12379
12719
  fs,
12380
12720
  fetch: scraperFetch,
12381
12721
  scrapers: await $provideScrapersForNode({ fs, llm, executables }, prepareAndScrapeOptions),
12382
- script: [new JavascriptExecutionTools(options)],
12722
+ script: [new JavascriptExecutionTools(cliOptions)],
12383
12723
  };
12384
12724
  if (isVerbose) {
12385
12725
  console.info(colors.gray('--- Getting the book ---'));
@@ -12543,11 +12883,12 @@ function $initializeRunCommand(program) {
12543
12883
  * @public exported from `@promptbook/remote-server`
12544
12884
  */
12545
12885
  function startRemoteServer(options) {
12546
- const { port, collection, createLlmExecutionTools, isAnonymousModeAllowed, isApplicationModeAllowed, isVerbose = DEFAULT_IS_VERBOSE, } = {
12886
+ const { port, collection, createLlmExecutionTools, isAnonymousModeAllowed, isApplicationModeAllowed, isVerbose = DEFAULT_IS_VERBOSE, login, } = {
12547
12887
  isAnonymousModeAllowed: false,
12548
12888
  isApplicationModeAllowed: false,
12549
12889
  collection: null,
12550
12890
  createLlmExecutionTools: null,
12891
+ login: null,
12551
12892
  ...options,
12552
12893
  };
12553
12894
  // <- TODO: [๐Ÿฆช] Some helper type to be able to use discriminant union types with destructuring
@@ -12614,9 +12955,38 @@ function startRemoteServer(options) {
12614
12955
  response.setHeader('X-Powered-By', 'Promptbook engine');
12615
12956
  next();
12616
12957
  });
12958
+ const swaggerOptions = {
12959
+ definition: {
12960
+ openapi: '3.0.0',
12961
+ info: {
12962
+ title: 'Promptbook Remote Server API',
12963
+ version: '1.0.0',
12964
+ description: 'API documentation for the Promptbook Remote Server',
12965
+ },
12966
+ servers: [
12967
+ {
12968
+ url: `http://localhost:${port}${rootPath}`,
12969
+ // <- TODO: !!!!! Probbably: Pass `remoteServerUrl` instead of `port` and `rootPath`
12970
+ },
12971
+ ],
12972
+ },
12973
+ apis: ['./src/remote-server/**/*.ts'], // Adjust path as needed
12974
+ };
12975
+ const swaggerSpec = swaggerJsdoc(swaggerOptions);
12976
+ app.use([`/api-docs`, `${rootPath}/api-docs`], swaggerUi.serve, swaggerUi.setup(swaggerSpec));
12617
12977
  const runningExecutionTasks = [];
12618
12978
  // <- TODO: [๐Ÿคฌ] Identify the users
12619
12979
  // TODO: [๐Ÿง ] Do here some garbage collection of finished tasks
12980
+ /**
12981
+ * @swagger
12982
+ * /:
12983
+ * get:
12984
+ * summary: Get server details
12985
+ * description: Returns details about the Promptbook server.
12986
+ * responses:
12987
+ * 200:
12988
+ * description: Server details in markdown format.
12989
+ */
12620
12990
  app.get(['/', rootPath], async (request, response) => {
12621
12991
  var _a;
12622
12992
  if ((_a = request.url) === null || _a === void 0 ? void 0 : _a.includes('socket.io')) {
@@ -12653,9 +13023,12 @@ function startRemoteServer(options) {
12653
13023
 
12654
13024
  ## Paths
12655
13025
 
12656
- ${block(app._router.stack
12657
- .map(({ route }) => (route === null || route === void 0 ? void 0 : route.path) || null)
12658
- .filter((path) => path !== null)
13026
+ ${block([
13027
+ ...app._router.stack
13028
+ .map(({ route }) => (route === null || route === void 0 ? void 0 : route.path) || null)
13029
+ .filter((path) => path !== null),
13030
+ '/api-docs',
13031
+ ]
12659
13032
  .map((path) => `- ${path}`)
12660
13033
  .join('\n'))}
12661
13034
 
@@ -12673,8 +13046,81 @@ function startRemoteServer(options) {
12673
13046
  https://github.com/webgptorg/promptbook
12674
13047
  `));
12675
13048
  });
12676
- // TODO: !!!!!! Add login route
12677
- app.get(`${rootPath}/books`, async (request, response) => {
13049
+ /**
13050
+ * @swagger
13051
+ *
13052
+ * /login:
13053
+ * post:
13054
+ * summary: Login to the server
13055
+ * description: Login to the server and get identification.
13056
+ * requestBody:
13057
+ * required: true
13058
+ * content:
13059
+ * application/json:
13060
+ * schema:
13061
+ * type: object
13062
+ * properties:
13063
+ * username:
13064
+ * type: string
13065
+ * password:
13066
+ * type: string
13067
+ * appId:
13068
+ * type: string
13069
+ * responses:
13070
+ * 200:
13071
+ * description: Successful login
13072
+ * content:
13073
+ * application/json:
13074
+ * schema:
13075
+ * type: object
13076
+ * properties:
13077
+ * identification:
13078
+ * type: object
13079
+ */
13080
+ app.post([`/login`, `${rootPath}/login`], async (request, response) => {
13081
+ if (!isApplicationModeAllowed || login === null) {
13082
+ response.status(400).send('Application mode is not allowed');
13083
+ return;
13084
+ }
13085
+ try {
13086
+ const username = request.body.username;
13087
+ const password = request.body.password;
13088
+ const appId = request.body.appId;
13089
+ const identification = await login({ username, password, appId });
13090
+ response.status(201).send({ identification });
13091
+ return;
13092
+ }
13093
+ catch (error) {
13094
+ if (!(error instanceof Error)) {
13095
+ throw error;
13096
+ }
13097
+ if (error instanceof AuthenticationError) {
13098
+ response.status(401).send({ error: serializeError(error) });
13099
+ }
13100
+ console.warn(`Login function thrown different error than AuthenticationError`, {
13101
+ error,
13102
+ serializedError: serializeError(error),
13103
+ });
13104
+ response.status(400).send({ error: serializeError(error) });
13105
+ }
13106
+ });
13107
+ /**
13108
+ * @swagger
13109
+ * /books:
13110
+ * get:
13111
+ * summary: List all books
13112
+ * description: Returns a list of all available books in the collection.
13113
+ * responses:
13114
+ * 200:
13115
+ * description: A list of books.
13116
+ * content:
13117
+ * application/json:
13118
+ * schema:
13119
+ * type: array
13120
+ * items:
13121
+ * type: string
13122
+ */
13123
+ app.get([`/books`, `${rootPath}/books`], async (request, response) => {
12678
13124
  if (collection === null) {
12679
13125
  response.status(500).send('No collection available');
12680
13126
  return;
@@ -12684,7 +13130,30 @@ function startRemoteServer(options) {
12684
13130
  response.send(pipelines);
12685
13131
  });
12686
13132
  // TODO: [๐Ÿง ] Is it secure / good idea to expose source codes of hosted books
12687
- app.get(`${rootPath}/books/*`, async (request, response) => {
13133
+ /**
13134
+ * @swagger
13135
+ * /books/{bookId}:
13136
+ * get:
13137
+ * summary: Get book content
13138
+ * description: Returns the content of a specific book.
13139
+ * parameters:
13140
+ * - in: path
13141
+ * name: bookId
13142
+ * required: true
13143
+ * schema:
13144
+ * type: string
13145
+ * description: The ID of the book to retrieve.
13146
+ * responses:
13147
+ * 200:
13148
+ * description: The content of the book.
13149
+ * content:
13150
+ * text/markdown:
13151
+ * schema:
13152
+ * type: string
13153
+ * 404:
13154
+ * description: Book not found.
13155
+ */
13156
+ app.get([`/books/*`, `${rootPath}/books/*`], async (request, response) => {
12688
13157
  try {
12689
13158
  if (collection === null) {
12690
13159
  response.status(500).send('No collection nor books available');
@@ -12738,10 +13207,26 @@ function startRemoteServer(options) {
12738
13207
  };
12739
13208
  }
12740
13209
  }
12741
- app.get(`${rootPath}/executions`, async (request, response) => {
13210
+ /**
13211
+ * @swagger
13212
+ * /executions:
13213
+ * get:
13214
+ * summary: List all executions
13215
+ * description: Returns a list of all running execution tasks.
13216
+ * responses:
13217
+ * 200:
13218
+ * description: A list of execution tasks.
13219
+ * content:
13220
+ * application/json:
13221
+ * schema:
13222
+ * type: array
13223
+ * items:
13224
+ * type: object
13225
+ */
13226
+ app.get([`/executions`, `${rootPath}/executions`], async (request, response) => {
12742
13227
  response.send(runningExecutionTasks.map((runningExecutionTask) => exportExecutionTask(runningExecutionTask, false)));
12743
13228
  });
12744
- app.get(`${rootPath}/executions/last`, async (request, response) => {
13229
+ app.get([`/executions/last`, `${rootPath}/executions/last`], async (request, response) => {
12745
13230
  // TODO: [๐Ÿคฌ] Filter only for user
12746
13231
  if (runningExecutionTasks.length === 0) {
12747
13232
  response.status(404).send('No execution tasks found');
@@ -12750,7 +13235,7 @@ function startRemoteServer(options) {
12750
13235
  const lastExecutionTask = runningExecutionTasks[runningExecutionTasks.length - 1];
12751
13236
  response.send(exportExecutionTask(lastExecutionTask, true));
12752
13237
  });
12753
- app.get(`${rootPath}/executions/:taskId`, async (request, response) => {
13238
+ app.get([`/executions/:taskId`, `${rootPath}/executions/:taskId`], async (request, response) => {
12754
13239
  const { taskId } = request.params;
12755
13240
  // TODO: [๐Ÿคฌ] Filter only for user
12756
13241
  const executionTask = runningExecutionTasks.find((executionTask) => executionTask.taskId === taskId);
@@ -12762,7 +13247,36 @@ function startRemoteServer(options) {
12762
13247
  }
12763
13248
  response.send(exportExecutionTask(executionTask, true));
12764
13249
  });
12765
- app.post(`${rootPath}/executions/new`, async (request, response) => {
13250
+ /**
13251
+ * @swagger
13252
+ * /executions/new:
13253
+ * post:
13254
+ * summary: Start a new execution
13255
+ * description: Starts a new execution task for a given pipeline.
13256
+ * requestBody:
13257
+ * required: true
13258
+ * content:
13259
+ * application/json:
13260
+ * schema:
13261
+ * type: object
13262
+ * properties:
13263
+ * pipelineUrl:
13264
+ * type: string
13265
+ * inputParameters:
13266
+ * type: object
13267
+ * identification:
13268
+ * type: object
13269
+ * responses:
13270
+ * 200:
13271
+ * description: The newly created execution task.
13272
+ * content:
13273
+ * application/json:
13274
+ * schema:
13275
+ * type: object
13276
+ * 400:
13277
+ * description: Invalid input.
13278
+ */
13279
+ app.post([`/executions/new`, `${rootPath}/executions/new`], async (request, response) => {
12766
13280
  try {
12767
13281
  const { inputParameters, identification /* <- [๐Ÿคฌ] */ } = request.body;
12768
13282
  const pipelineUrl = request.body.pipelineUrl || request.body.book;
@@ -12998,12 +13512,12 @@ function $initializeStartServerCommand(program) {
12998
13512
  `));
12999
13513
  startServerCommand.option('--allow-anonymous', `Is anonymous mode allowed`, false);
13000
13514
  startServerCommand.option('-r, --reload', `Call LLM models even if same prompt with result is in the cache`, false);
13001
- startServerCommand.option('-v, --verbose', `Is output verbose`, false);
13002
13515
  startServerCommand.description(spaceTrim(`
13003
13516
  Starts a remote server to execute books
13004
13517
  `));
13005
13518
  startServerCommand.alias('server');
13006
- startServerCommand.action(handleActionErrors(async (path, { port: portRaw, url: rawUrl, allowAnonymous: isAnonymousModeAllowed, reload: isCacheReloaded, verbose: isVerbose, }) => {
13519
+ startServerCommand.action(handleActionErrors(async (path, cliOptions) => {
13520
+ const { port: portRaw, url: rawUrl, allowAnonymous: isAnonymousModeAllowed, reload: isCacheReloaded, verbose: isVerbose, } = cliOptions;
13007
13521
  if (rawUrl && !isValidUrl(rawUrl)) {
13008
13522
  console.error(colors.red(`Invalid URL: ${rawUrl}`));
13009
13523
  return process.exit(1);
@@ -13032,7 +13546,7 @@ function $initializeStartServerCommand(program) {
13032
13546
  isCacheReloaded,
13033
13547
  }; /* <- TODO: ` satisfies PrepareAndScrapeOptions` */
13034
13548
  const fs = $provideFilesystemForNode(prepareAndScrapeOptions);
13035
- const llm = await $provideLlmToolsForWizzardOrCli(prepareAndScrapeOptions);
13549
+ const llm = await $provideLlmToolsForCli({ cliOptions, ...prepareAndScrapeOptions });
13036
13550
  const executables = await $provideExecutablesForNode(prepareAndScrapeOptions);
13037
13551
  const tools = {
13038
13552
  llm,
@@ -13056,6 +13570,9 @@ function $initializeStartServerCommand(program) {
13056
13570
  isAnonymousModeAllowed,
13057
13571
  isApplicationModeAllowed: true,
13058
13572
  collection,
13573
+ async login() {
13574
+ throw new AuthenticationError('You can not login to the server started by `ptbk start-server` in cli, use `startRemoteServer` function instead.');
13575
+ },
13059
13576
  createLlmExecutionTools(options) {
13060
13577
  const { appId, userId } = options;
13061
13578
  TODO_USE({ appId, userId });
@@ -13092,8 +13609,8 @@ function $initializeTestCommand(program) {
13092
13609
  testCommand.option('--no-validation', `Do not validate logic of pipelines in collection`, true);
13093
13610
  testCommand.option('--no-prepare', `Do not prepare the pipelines, ideal when no LLM tools or scrapers available`, true);
13094
13611
  testCommand.option('-r, --reload', `Call LLM models even if same prompt with result is in the cache `, false);
13095
- testCommand.option('-v, --verbose', `Is output verbose`, false);
13096
- testCommand.action(handleActionErrors(async (filesGlob, { ignore: ignoreRaw = '', validation: isValidated, prepare: isPrepared, reload: isCacheReloaded, verbose: isVerbose, }) => {
13612
+ testCommand.action(handleActionErrors(async (filesGlob, cliOptions) => {
13613
+ const { ignore: ignoreRaw = '', validation: isValidated, prepare: isPrepared, reload: isCacheReloaded, verbose: isVerbose, } = cliOptions;
13097
13614
  let tools = undefined;
13098
13615
  if (isPrepared) {
13099
13616
  // TODO: DRY [โ—ฝ]
@@ -13102,7 +13619,7 @@ function $initializeTestCommand(program) {
13102
13619
  isCacheReloaded,
13103
13620
  }; /* <- TODO: ` satisfies PrepareAndScrapeOptions` */
13104
13621
  const fs = $provideFilesystemForNode(prepareAndScrapeOptions);
13105
- const llm = await $provideLlmToolsForWizzardOrCli(prepareAndScrapeOptions);
13622
+ const llm = await $provideLlmToolsForCli({ cliOptions, ...prepareAndScrapeOptions });
13106
13623
  const executables = await $provideExecutablesForNode(prepareAndScrapeOptions);
13107
13624
  tools = {
13108
13625
  llm,
@@ -13163,56 +13680,16 @@ function $initializeTestCommand(program) {
13163
13680
  */
13164
13681
 
13165
13682
  /**
13166
- * Initializes `login` command for Promptbook CLI utilities
13167
- *
13168
- * Note: `$` is used to indicate that this function is not a pure function - it registers a command in the CLI
13683
+ * Note: `$` is used to indicate that this function is not a pure function - it registers an option in the CLI
13169
13684
  *
13170
- * @private internal function of `promptbookCli`
13685
+ * @private utility of CLI
13171
13686
  */
13172
- function $initializeLoginCommand(program) {
13173
- const loginCommand = program.command('login');
13174
- loginCommand.description(spaceTrim(`
13175
- Login to the remote Promptbook server
13176
- `));
13177
- loginCommand.action(handleActionErrors(async () => {
13178
- // @@@
13179
- console.error(colors.green(spaceTrim(`
13180
- You will be logged in to https://promptbook.studio server.
13181
- If you don't have an account, it will be created automatically.
13182
- `)));
13183
- const { email, password } = await prompts([
13184
- {
13185
- type: 'text',
13186
- name: 'email',
13187
- message: 'Enter your email:',
13188
- validate: (value) => (isValidEmail(value) ? true : 'Valid email is required'),
13189
- },
13190
- {
13191
- type: 'password',
13192
- name: 'password',
13193
- message: 'Enter your password:',
13194
- validate: (value) => value.length /* <- TODO: [๐Ÿง ] Better password validation */ > 0 ? true : 'Password is required',
13195
- },
13196
- ]);
13197
- TODO_USE(email, password);
13198
- await forTime(1000);
13199
- console.error(colors.green(spaceTrim(`
13200
- Your account ${email} was successfully created.
13201
-
13202
- Please verify your email:
13203
- https://brj.app/api/v1/customer/register-account?apiKey=PRODdh003eNKaec7PoO1AzU244tsL4WO
13204
-
13205
- After verification, you will receive 500 000 credits for free ๐ŸŽ‰
13206
- `)));
13207
- return process.exit(0);
13208
- }));
13687
+ function $addGlobalOptionsToCommand(command) {
13688
+ command.option('-v, --verbose', `Log more details`, false);
13689
+ 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`);
13690
+ command.option('-p, --provider <provider>', `Which LLM provider to use: "BYOK" / "BRING_YOUR_OWN_KEYS" or "REMOTE_SERVER" / "RS"`, 'REMOTE_SERVER');
13691
+ command.option('--remote-server-url <url>', `URL of remote server to use when `, DEFAULT_REMOTE_SERVER_URL);
13209
13692
  }
13210
- /**
13211
- * TODO: Pass remote server URL (and path)
13212
- * TODO: Implement non-interactive login
13213
- * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
13214
- * Note: [๐ŸŸก] Code in this file should never be published outside of `@promptbook/cli`
13215
- */
13216
13693
 
13217
13694
  /**
13218
13695
  * Runs CLI utilities of Promptbook package
@@ -13238,6 +13715,7 @@ async function promptbookCli() {
13238
13715
  program.alias('ptbk');
13239
13716
  program.version(PROMPTBOOK_ENGINE_VERSION);
13240
13717
  program.description(CLAIM);
13718
+ // Note: Theese options are valid for all commands
13241
13719
  $initializeAboutCommand(program);
13242
13720
  $initializeRunCommand(program);
13243
13721
  $initializeLoginCommand(program);
@@ -13248,6 +13726,8 @@ async function promptbookCli() {
13248
13726
  $initializeListModelsCommand(program);
13249
13727
  $initializeListScrapersCommand(program);
13250
13728
  $initializeStartServerCommand(program);
13729
+ // TODO: [๐Ÿง ] Should it be here or not> $addGlobalOptionsToCommand(program);
13730
+ program.commands.forEach($addGlobalOptionsToCommand);
13251
13731
  program.parse(process.argv);
13252
13732
  }
13253
13733
  /**
@@ -13297,8 +13777,7 @@ const _AnthropicClaudeMetadataRegistration = $llmToolsMetadataRegister.register(
13297
13777
  options: {
13298
13778
  apiKey: 'sk-ant-api03-',
13299
13779
  isProxied: true,
13300
- remoteUrl: DEFAULT_REMOTE_URL,
13301
- path: DEFAULT_REMOTE_URL_PATH,
13780
+ remoteServerUrl: DEFAULT_REMOTE_SERVER_URL,
13302
13781
  },
13303
13782
  };
13304
13783
  },
@@ -13321,146 +13800,6 @@ const _AnthropicClaudeMetadataRegistration = $llmToolsMetadataRegister.register(
13321
13800
  * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
13322
13801
  */
13323
13802
 
13324
- /**
13325
- * Creates a connection to the remote proxy server.
13326
- *
13327
- * Note: This function creates a connection to the remote server and returns a socket but responsibility of closing the connection is on the caller
13328
- *
13329
- * @private internal utility function
13330
- */
13331
- async function createRemoteClient(options) {
13332
- const { remoteUrl, path } = options;
13333
- return new Promise((resolve, reject) => {
13334
- const socket = io(remoteUrl, {
13335
- retries: CONNECTION_RETRIES_LIMIT,
13336
- timeout: CONNECTION_TIMEOUT_MS,
13337
- path,
13338
- // path: `${this.remoteUrl.pathname}/socket.io`,
13339
- transports: [/*'websocket', <- TODO: [๐ŸŒฌ] Make websocket transport work */ 'polling'],
13340
- });
13341
- // console.log('Connecting to', this.options.remoteUrl.href, { socket });
13342
- socket.on('connect', () => {
13343
- resolve(socket);
13344
- });
13345
- // TODO: [๐Ÿ’ฉ] Better timeout handling
13346
- setTimeout(() => {
13347
- reject(new Error(`Timeout while connecting to ${remoteUrl}`));
13348
- }, CONNECTION_TIMEOUT_MS);
13349
- });
13350
- }
13351
-
13352
- /**
13353
- * Remote server is a proxy server that uses its execution tools internally and exposes the executor interface externally.
13354
- *
13355
- * You can simply use `RemoteExecutionTools` on client-side javascript and connect to your remote server.
13356
- * This is useful to make all logic on browser side but not expose your API keys or no need to use customer's GPU.
13357
- *
13358
- * @see https://github.com/webgptorg/promptbook#remote-server
13359
- * @public exported from `@promptbook/remote-client`
13360
- */
13361
- class RemoteLlmExecutionTools {
13362
- /* <- TODO: [๐Ÿš] `, Destroyable` */
13363
- constructor(options) {
13364
- this.options = options;
13365
- }
13366
- get title() {
13367
- // TODO: [๐Ÿง ] Maybe fetch title+description from the remote server (as well as if model methods are defined)
13368
- return 'Remote server';
13369
- }
13370
- get description() {
13371
- return 'Use all models by your remote server';
13372
- }
13373
- /**
13374
- * Check the configuration of all execution tools
13375
- */
13376
- async checkConfiguration() {
13377
- const socket = await createRemoteClient(this.options);
13378
- socket.disconnect();
13379
- // TODO: [main] !!3 Check version of the remote server and compatibility
13380
- // TODO: [๐ŸŽ] Send checkConfiguration
13381
- }
13382
- /**
13383
- * List all available models that can be used
13384
- */
13385
- async listModels() {
13386
- // TODO: [๐Ÿ‘’] Listing models (and checking configuration) probbably should go through REST API not Socket.io
13387
- const socket = await createRemoteClient(this.options);
13388
- socket.emit('listModels-request', {
13389
- identification: this.options.identification,
13390
- } /* <- Note: [๐Ÿค›] */);
13391
- const promptResult = await new Promise((resolve, reject) => {
13392
- socket.on('listModels-response', (response) => {
13393
- resolve(response.models);
13394
- socket.disconnect();
13395
- });
13396
- socket.on('error', (error) => {
13397
- reject(deserializeError(error));
13398
- socket.disconnect();
13399
- });
13400
- });
13401
- socket.disconnect();
13402
- return promptResult;
13403
- }
13404
- /**
13405
- * Calls remote proxy server to use a chat model
13406
- */
13407
- callChatModel(prompt) {
13408
- if (this.options.isVerbose) {
13409
- console.info(`๐Ÿ–‹ Remote callChatModel call`);
13410
- }
13411
- return /* not await */ this.callCommonModel(prompt);
13412
- }
13413
- /**
13414
- * Calls remote proxy server to use a completion model
13415
- */
13416
- callCompletionModel(prompt) {
13417
- if (this.options.isVerbose) {
13418
- console.info(`๐Ÿ’ฌ Remote callCompletionModel call`);
13419
- }
13420
- return /* not await */ this.callCommonModel(prompt);
13421
- }
13422
- /**
13423
- * Calls remote proxy server to use a embedding model
13424
- */
13425
- callEmbeddingModel(prompt) {
13426
- if (this.options.isVerbose) {
13427
- console.info(`๐Ÿ’ฌ Remote callEmbeddingModel call`);
13428
- }
13429
- return /* not await */ this.callCommonModel(prompt);
13430
- }
13431
- // <- Note: [๐Ÿค–] callXxxModel
13432
- /**
13433
- * Calls remote proxy server to use both completion or chat model
13434
- */
13435
- async callCommonModel(prompt) {
13436
- const socket = await createRemoteClient(this.options);
13437
- socket.emit('prompt-request', {
13438
- identification: this.options.identification,
13439
- prompt,
13440
- } /* <- Note: [๐Ÿค›] */);
13441
- const promptResult = await new Promise((resolve, reject) => {
13442
- socket.on('prompt-response', (response) => {
13443
- resolve(response.promptResult);
13444
- socket.disconnect();
13445
- });
13446
- socket.on('error', (error) => {
13447
- reject(deserializeError(error));
13448
- socket.disconnect();
13449
- });
13450
- });
13451
- socket.disconnect();
13452
- return promptResult;
13453
- }
13454
- }
13455
- /**
13456
- * TODO: Maybe use `$exportJson`
13457
- * TODO: [๐Ÿง ][๐Ÿ›] Maybe not `isAnonymous: boolean` BUT `mode: 'ANONYMOUS'|'COLLECTION'`
13458
- * TODO: [๐Ÿ“] Allow to list compatible models with each variant
13459
- * TODO: [๐Ÿ—ฏ] RemoteLlmExecutionTools should extend Destroyable and implement IDestroyable
13460
- * TODO: [๐Ÿง ][๐ŸŒฐ] Allow to pass `title` for tracking purposes
13461
- * TODO: [๐Ÿง ] Maybe remove `@promptbook/remote-client` and just use `@promptbook/core`
13462
- */
13463
-
13464
13803
  /**
13465
13804
  * Function computeUsage will create price per one token based on the string value found on openai page
13466
13805
  *