@promptbook/cli 0.89.0-6 โ†’ 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,14 +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
26
  import swaggerJsdoc from 'swagger-jsdoc';
26
27
  import swaggerUi from 'swagger-ui-express';
27
- import { io } from 'socket.io-client';
28
28
  import Anthropic from '@anthropic-ai/sdk';
29
29
  import { OpenAIClient, AzureKeyCredential } from '@azure/openai';
30
30
  import OpenAI from 'openai';
@@ -46,7 +46,7 @@ const BOOK_LANGUAGE_VERSION = '1.0.0';
46
46
  * @generated
47
47
  * @see https://github.com/webgptorg/promptbook
48
48
  */
49
- const PROMPTBOOK_ENGINE_VERSION = '0.89.0-6';
49
+ const PROMPTBOOK_ENGINE_VERSION = '0.89.0-7';
50
50
  /**
51
51
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
52
52
  * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
@@ -215,6 +215,7 @@ const DEFAULT_MAX_EXECUTION_ATTEMPTS = 10; // <- TODO: [๐Ÿคนโ€โ™‚๏ธ]
215
215
  */
216
216
  const DEFAULT_BOOKS_DIRNAME = './books';
217
217
  // <- TODO: [๐Ÿ•] Make also `BOOKS_DIRNAME_ALTERNATIVES`
218
+ // TODO: !!!!!! Just .promptbook dir, hardocode others
218
219
  /**
219
220
  * Where to store the temporary downloads
220
221
  *
@@ -239,6 +240,21 @@ const DEFAULT_EXECUTION_CACHE_DIRNAME = './.promptbook/execution-cache';
239
240
  * @public exported from `@promptbook/core`
240
241
  */
241
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
+ */
242
258
  /**
243
259
  * The name of the builded pipeline collection made by CLI `ptbk make` and for lookup in `createCollectionFromDirectory`
244
260
  *
@@ -259,13 +275,7 @@ const MOMENT_ARG_THRESHOLDS = {
259
275
  *
260
276
  * @public exported from `@promptbook/core`
261
277
  */
262
- const DEFAULT_REMOTE_URL = 'https://api.pavolhejny.com/';
263
- /**
264
- * @@@
265
- *
266
- * @public exported from `@promptbook/core`
267
- */
268
- const DEFAULT_REMOTE_URL_PATH = '/promptbook/socket.io';
278
+ const DEFAULT_REMOTE_SERVER_URL = 'https://api.pavolhejny.com/promptbook';
269
279
  // <- TODO: [๐Ÿงœโ€โ™‚๏ธ]
270
280
  /**
271
281
  * @@@
@@ -307,7 +317,7 @@ const IS_PIPELINE_LOGIC_VALIDATED = just(
307
317
  true);
308
318
  /**
309
319
  * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
310
- * TODO: [๐Ÿง ][๐Ÿงœโ€โ™‚๏ธ] Maybe join remoteUrl and path into single value
320
+ * TODO: [๐Ÿง ][๐Ÿงœโ€โ™‚๏ธ] Maybe join remoteServerUrl and path into single value
311
321
  */
312
322
 
313
323
  /**
@@ -485,7 +495,8 @@ function $initializeHelloCommand(program) {
485
495
  helloCommand.alias('hi');
486
496
  helloCommand.argument('[name]', 'Your name', 'Paul');
487
497
  helloCommand.option('-g, --greeting <greeting>', `Greeting`, 'Hello');
488
- helloCommand.action(handleActionErrors(async (name, { greeting }) => {
498
+ helloCommand.action(handleActionErrors(async (name, cliOptions) => {
499
+ const { greeting } = cliOptions;
489
500
  console.info(colors.cyan(`${greeting} ${name}`));
490
501
  await forTime(1000);
491
502
  console.info(colors.rainbow(`Nice to meet you!`));
@@ -499,40 +510,27 @@ function $initializeHelloCommand(program) {
499
510
  */
500
511
 
501
512
  /**
502
- * Just marks a place of place where should be something implemented
503
- * No side effects.
504
- *
505
- * 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
506
514
  *
507
- * @param value any values
508
- * @returns void
509
- * @private within the repository
515
+ * @public exported from `@promptbook/core`
510
516
  */
511
- function TODO_USE(...value) {
512
- }
517
+ class NotYetImplementedError extends Error {
518
+ constructor(message) {
519
+ super(spaceTrim$1((block) => `
520
+ ${block(message)}
513
521
 
514
- /**
515
- * @@@
516
- *
517
- * @public exported from `@promptbook/node`
518
- */
519
- function $provideFilesystemForNode(options) {
520
- if (!$isRunningInNode()) {
521
- 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);
522
532
  }
523
- return {
524
- stat,
525
- access,
526
- constants,
527
- readFile,
528
- writeFile,
529
- readdir,
530
- mkdir,
531
- };
532
533
  }
533
- /**
534
- * Note: [๐ŸŸข] Code in this file should never be never released in packages that could be imported into browser environment
535
- */
536
534
 
537
535
  /**
538
536
  * Make error report URL for the given error
@@ -603,120 +601,566 @@ class UnexpectedError extends Error {
603
601
  }
604
602
 
605
603
  /**
606
- * Orders JSON object by keys
604
+ * @@@
607
605
  *
608
- * @returns The same type of object as the input re-ordered
609
- * @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`
610
609
  */
611
- function orderJson(options) {
612
- const { value, order } = options;
613
- const orderedValue = {
614
- ...(order === undefined ? {} : Object.fromEntries(order.map((key) => [key, undefined]))),
615
- ...value,
616
- };
617
- return orderedValue;
610
+ function $getGlobalScope() {
611
+ return Function('return this')();
618
612
  }
619
613
 
620
614
  /**
621
- * Freezes the given object and all its nested objects recursively
622
- *
623
- * Note: `$` is used to indicate that this function is not a pure function - it mutates given object
624
- * Note: This function mutates the object and returns the original (but mutated-deep-freezed) object
615
+ * @@@
625
616
  *
626
- * @returns The same object as the input, but deeply frozen
617
+ * @param text @@@
618
+ * @returns @@@
619
+ * @example 'HELLO_WORLD'
620
+ * @example 'I_LOVE_PROMPTBOOK'
627
621
  * @public exported from `@promptbook/utils`
628
622
  */
629
- function $deepFreeze(objectValue) {
630
- if (Array.isArray(objectValue)) {
631
- return Object.freeze(objectValue.map((item) => $deepFreeze(item)));
632
- }
633
- const propertyNames = Object.getOwnPropertyNames(objectValue);
634
- for (const propertyName of propertyNames) {
635
- const value = objectValue[propertyName];
636
- if (value && typeof value === 'object') {
637
- $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 += '_';
638
650
  }
651
+ normalizedName += normalizedChar;
652
+ lastCharType = charType;
639
653
  }
640
- Object.freeze(objectValue);
641
- return objectValue;
654
+ normalizedName = normalizedName.replace(/_+/g, '_');
655
+ normalizedName = normalizedName.replace(/_?\/_?/g, '/');
656
+ normalizedName = normalizedName.replace(/^_/, '');
657
+ normalizedName = normalizedName.replace(/_$/, '');
658
+ return normalizedName;
642
659
  }
643
660
  /**
644
- * 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
645
667
  */
646
668
 
647
669
  /**
648
- * Checks if the value is [๐Ÿš‰] serializable as JSON
649
- * If not, throws an UnexpectedError with a rich error message and tracking
650
- *
651
- * - Almost all primitives are serializable BUT:
652
- * - `undefined` is not serializable
653
- * - `NaN` is not serializable
654
- * - Objects and arrays are serializable if all their properties are serializable
655
- * - Functions are not serializable
656
- * - Circular references are not serializable
657
- * - `Date` objects are not serializable
658
- * - `Map` and `Set` objects are not serializable
659
- * - `RegExp` objects are not serializable
660
- * - `Error` objects are not serializable
661
- * - `Symbol` objects are not serializable
662
- * - And much more...
670
+ * @@@
663
671
  *
664
- * @throws UnexpectedError if the value is not serializable as JSON
672
+ * @param text @@@
673
+ * @returns @@@
674
+ * @example 'hello_world'
675
+ * @example 'i_love_promptbook'
665
676
  * @public exported from `@promptbook/utils`
666
677
  */
667
- function checkSerializableAsJson(options) {
668
- const { value, name, message } = options;
669
- if (value === undefined) {
670
- throw new UnexpectedError(`${name} is undefined`);
671
- }
672
- else if (value === null) {
673
- return;
674
- }
675
- else if (typeof value === 'boolean') {
676
- return;
677
- }
678
- else if (typeof value === 'number' && !isNaN(value)) {
679
- return;
680
- }
681
- else if (typeof value === 'string') {
682
- return;
683
- }
684
- else if (typeof value === 'symbol') {
685
- throw new UnexpectedError(`${name} is symbol`);
686
- }
687
- else if (typeof value === 'function') {
688
- throw new UnexpectedError(`${name} is function`);
689
- }
690
- else if (typeof value === 'object' && Array.isArray(value)) {
691
- for (let i = 0; i < value.length; i++) {
692
- checkSerializableAsJson({ name: `${name}[${i}]`, value: value[i], message });
693
- }
694
- }
695
- else if (typeof value === 'object') {
696
- if (value instanceof Date) {
697
- throw new UnexpectedError(spaceTrim((block) => `
698
- \`${name}\` is Date
699
-
700
- Use \`string_date_iso8601\` instead
678
+ function normalizeTo_snake_case(text) {
679
+ return normalizeTo_SCREAMING_CASE(text).toLowerCase();
680
+ }
701
681
 
702
- Additional message for \`${name}\`:
703
- ${block(message || '(nothing)')}
704
- `));
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] = [];
705
696
  }
706
- else if (value instanceof Map) {
707
- 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]}`);
708
699
  }
709
- else if (value instanceof Set) {
710
- 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);
711
712
  }
712
- else if (value instanceof RegExp) {
713
- throw new UnexpectedError(`${name} is RegExp`);
713
+ else {
714
+ this.storage[existingRegistrationIndex] = registered;
714
715
  }
715
- else if (value instanceof Error) {
716
- throw new UnexpectedError(spaceTrim((block) => `
717
- \`${name}\` is unserialized Error
718
-
719
- 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\`
720
1164
 
721
1165
  Additional message for \`${name}\`:
722
1166
  ${block(message || '(nothing)')}
@@ -1060,35 +1504,6 @@ function isValidFilePath(filename) {
1060
1504
  * TODO: [๐Ÿ] Implement for MacOs
1061
1505
  */
1062
1506
 
1063
- /**
1064
- * Tests if given string is valid URL.
1065
- *
1066
- * Note: Dataurl are considered perfectly valid.
1067
- * Note: There are two simmilar functions:
1068
- * - `isValidUrl` which tests any URL
1069
- * - `isValidPipelineUrl` *(this one)* which tests just promptbook URL
1070
- *
1071
- * @public exported from `@promptbook/utils`
1072
- */
1073
- function isValidUrl(url) {
1074
- if (typeof url !== 'string') {
1075
- return false;
1076
- }
1077
- try {
1078
- if (url.startsWith('blob:')) {
1079
- url = url.replace(/^blob:/, '');
1080
- }
1081
- const urlObject = new URL(url /* because fail is handled */);
1082
- if (!['http:', 'https:', 'data:'].includes(urlObject.protocol)) {
1083
- return false;
1084
- }
1085
- return true;
1086
- }
1087
- catch (error) {
1088
- return false;
1089
- }
1090
- }
1091
-
1092
1507
  const defaultDiacriticsRemovalMap = [
1093
1508
  {
1094
1509
  base: 'A',
@@ -1472,32 +1887,177 @@ class FileCacheStorage {
1472
1887
  // TODO: [๐ŸŒ—]
1473
1888
  return value;
1474
1889
  }
1475
- /**
1476
- * @@@ Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.
1477
- */
1478
- async setItem(key, value) {
1479
- const filename = this.getFilenameForKey(key);
1480
- if (!isSerializableAsJson(value)) {
1481
- throw new UnexpectedError(`The "${key}" you want to store in JSON file is not serializable as JSON`);
1482
- }
1483
- const fileContent = stringifyPipelineJson(value);
1484
- await mkdir(dirname(filename), { recursive: true }); // <- [0]
1485
- 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);
1486
2044
  }
1487
- /**
1488
- * @@@ 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.
1489
- */
1490
- async removeItem(key) {
1491
- const filename = this.getFilenameForKey(key);
1492
- // TODO: [๐Ÿง ] What to use `unlink` or `rm`
1493
- await unlink(filename);
1494
- // <- TODO: [๐Ÿฟ][๐Ÿง ] Maybe remove empty folders
1495
- // [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);
1496
2057
  }
1497
2058
  }
1498
2059
  /**
1499
- * TODO: [๐ŸŒ—] Maybe some checkers, not all valid JSONs are desired and valid values
1500
- * 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`
1501
2061
  */
1502
2062
 
1503
2063
  /**
@@ -1535,51 +2095,260 @@ class PipelineExecutionError extends Error {
1535
2095
  */
1536
2096
 
1537
2097
  /**
1538
- * 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)
1539
2099
  *
1540
2100
  * @public exported from `@promptbook/core`
1541
2101
  */
1542
- class MemoryStorage {
1543
- constructor() {
1544
- 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';
1545
2261
  }
1546
2262
  /**
1547
- * Returns the number of key/value pairs currently present in the list associated with the object.
2263
+ * Check the configuration of all execution tools
1548
2264
  */
1549
- get length() {
1550
- 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
1551
2270
  }
1552
2271
  /**
1553
- * 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
1554
2273
  */
1555
- clear() {
1556
- 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;
1557
2292
  }
1558
2293
  /**
1559
- * 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
1560
2295
  */
1561
- getItem(key) {
1562
- 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);
1563
2301
  }
1564
2302
  /**
1565
- * 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
1566
2304
  */
1567
- key(index) {
1568
- 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);
1569
2310
  }
1570
2311
  /**
1571
- * 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
1572
2313
  */
1573
- setItem(key, value) {
1574
- 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);
1575
2319
  }
2320
+ // <- Note: [๐Ÿค–] callXxxModel
1576
2321
  /**
1577
- * 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
1578
2323
  */
1579
- removeItem(key) {
1580
- 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;
1581
2342
  }
1582
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
+ */
1583
2352
 
1584
2353
  /**
1585
2354
  * Simple wrapper `new Date().toISOString()`
@@ -1858,346 +2627,32 @@ function countUsage(llmTools) {
1858
2627
  }
1859
2628
  // <- Note: [๐Ÿค–]
1860
2629
  return proxyTools;
1861
- }
1862
- /**
1863
- * TODO: [๐Ÿง ][๐Ÿ’ธ] Maybe make some common abstraction `interceptLlmTools` and use here (or use javascript Proxy?)
1864
- * TODO: [๐Ÿง ] Is there some meaningfull way how to test this util
1865
- * TODO: [๐Ÿง ][๐ŸŒฏ] Maybe a way how to hide ability to `get totalUsage`
1866
- * > const [llmToolsWithUsage,getUsage] = countTotalUsage(llmTools);
1867
- * TODO: [๐Ÿ‘ทโ€โ™‚๏ธ] @@@ Manual about construction of llmTools
1868
- */
1869
-
1870
- /**
1871
- * This error type indicates that some part of the code is not implemented yet
1872
- *
1873
- * @public exported from `@promptbook/core`
1874
- */
1875
- class NotYetImplementedError extends Error {
1876
- constructor(message) {
1877
- super(spaceTrim$1((block) => `
1878
- ${block(message)}
1879
-
1880
- Note: This feature is not implemented yet but it will be soon.
1881
-
1882
- If you want speed up the implementation or just read more, look here:
1883
- https://github.com/webgptorg/promptbook
1884
-
1885
- Or contact us on pavol@ptbk.io
1886
-
1887
- `));
1888
- this.name = 'NotYetImplementedError';
1889
- Object.setPrototypeOf(this, NotYetImplementedError.prototype);
1890
- }
1891
- }
1892
-
1893
- /**
1894
- * @@@
1895
- *
1896
- * Note: `$` is used to indicate that this function is not a pure function - it access global scope
1897
- *
1898
- * @private internal function of `$Register`
1899
- */
1900
- function $getGlobalScope() {
1901
- return Function('return this')();
1902
- }
1903
-
1904
- /**
1905
- * @@@
1906
- *
1907
- * @param text @@@
1908
- * @returns @@@
1909
- * @example 'HELLO_WORLD'
1910
- * @example 'I_LOVE_PROMPTBOOK'
1911
- * @public exported from `@promptbook/utils`
1912
- */
1913
- function normalizeTo_SCREAMING_CASE(text) {
1914
- let charType;
1915
- let lastCharType = 'OTHER';
1916
- let normalizedName = '';
1917
- for (const char of text) {
1918
- let normalizedChar;
1919
- if (/^[a-z]$/.test(char)) {
1920
- charType = 'LOWERCASE';
1921
- normalizedChar = char.toUpperCase();
1922
- }
1923
- else if (/^[A-Z]$/.test(char)) {
1924
- charType = 'UPPERCASE';
1925
- normalizedChar = char;
1926
- }
1927
- else if (/^[0-9]$/.test(char)) {
1928
- charType = 'NUMBER';
1929
- normalizedChar = char;
1930
- }
1931
- else {
1932
- charType = 'OTHER';
1933
- normalizedChar = '_';
1934
- }
1935
- if (charType !== lastCharType &&
1936
- !(lastCharType === 'UPPERCASE' && charType === 'LOWERCASE') &&
1937
- !(lastCharType === 'NUMBER') &&
1938
- !(charType === 'NUMBER')) {
1939
- normalizedName += '_';
1940
- }
1941
- normalizedName += normalizedChar;
1942
- lastCharType = charType;
1943
- }
1944
- normalizedName = normalizedName.replace(/_+/g, '_');
1945
- normalizedName = normalizedName.replace(/_?\/_?/g, '/');
1946
- normalizedName = normalizedName.replace(/^_/, '');
1947
- normalizedName = normalizedName.replace(/_$/, '');
1948
- return normalizedName;
1949
- }
1950
- /**
1951
- * TODO: Tests
1952
- * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: 'Moje tabule' })).toEqual('/VtG7sR9rRJqwNEdM2/Moje tabule');
1953
- * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: 'ฤ›ลกฤล™ลพลพรฝรกรญรบลฏ' })).toEqual('/VtG7sR9rRJqwNEdM2/escrzyaieuu');
1954
- * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: ' ahoj ' })).toEqual('/VtG7sR9rRJqwNEdM2/ahoj');
1955
- * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: ' ahoj_ahojAhoj ahoj ' })).toEqual('/VtG7sR9rRJqwNEdM2/ahoj-ahoj-ahoj-ahoj');
1956
- * TODO: [๐ŸŒบ] Use some intermediate util splitWords
1957
- */
1958
-
1959
- /**
1960
- * @@@
1961
- *
1962
- * @param text @@@
1963
- * @returns @@@
1964
- * @example 'hello_world'
1965
- * @example 'i_love_promptbook'
1966
- * @public exported from `@promptbook/utils`
1967
- */
1968
- function normalizeTo_snake_case(text) {
1969
- return normalizeTo_SCREAMING_CASE(text).toLowerCase();
1970
- }
1971
-
1972
- /**
1973
- * Register is @@@
1974
- *
1975
- * Note: `$` is used to indicate that this function is not a pure function - it accesses and adds variables in global scope.
1976
- *
1977
- * @private internal utility, exported are only signleton instances of this class
1978
- */
1979
- class $Register {
1980
- constructor(registerName) {
1981
- this.registerName = registerName;
1982
- const storageName = `_promptbook_${normalizeTo_snake_case(registerName)}`;
1983
- const globalScope = $getGlobalScope();
1984
- if (globalScope[storageName] === undefined) {
1985
- globalScope[storageName] = [];
1986
- }
1987
- else if (!Array.isArray(globalScope[storageName])) {
1988
- throw new UnexpectedError(`Expected (global) ${storageName} to be an array, but got ${typeof globalScope[storageName]}`);
1989
- }
1990
- this.storage = globalScope[storageName];
1991
- }
1992
- list() {
1993
- // <- TODO: ReadonlyDeep<ReadonlyArray<TRegistered>>
1994
- return this.storage;
1995
- }
1996
- register(registered) {
1997
- const { packageName, className } = registered;
1998
- const existingRegistrationIndex = this.storage.findIndex((item) => item.packageName === packageName && item.className === className);
1999
- const existingRegistration = this.storage[existingRegistrationIndex];
2000
- if (!existingRegistration) {
2001
- this.storage.push(registered);
2002
- }
2003
- else {
2004
- this.storage[existingRegistrationIndex] = registered;
2005
- }
2006
- return {
2007
- registerName: this.registerName,
2008
- packageName,
2009
- className,
2010
- get isDestroyed() {
2011
- return false;
2012
- },
2013
- destroy() {
2014
- throw new NotYetImplementedError(`Registration to ${this.registerName} is permanent in this version of Promptbook`);
2015
- },
2016
- };
2017
- }
2018
- }
2019
-
2020
- /**
2021
- * @@@
2022
- *
2023
- * Note: `$` is used to indicate that this interacts with the global scope
2024
- * @singleton Only one instance of each register is created per build, but thare can be more @@@
2025
- * @public exported from `@promptbook/core`
2026
- */
2027
- const $llmToolsMetadataRegister = new $Register('llm_tools_metadata');
2028
- /**
2029
- * TODO: [ยฎ] DRY Register logic
2030
- */
2031
-
2032
- /**
2033
- * Determines if the given path is a root path.
2034
- *
2035
- * Note: This does not check if the file exists only if the path is valid
2036
- * @public exported from `@promptbook/utils`
2037
- */
2038
- function isRootPath(value) {
2039
- if (value === '/') {
2040
- return true;
2041
- }
2042
- if (/^[A-Z]:\\$/i.test(value)) {
2043
- return true;
2044
- }
2045
- return false;
2046
- }
2047
- /**
2048
- * TODO: [๐Ÿ] Make for MacOS paths
2049
- */
2050
-
2051
- /**
2052
- * @@@
2053
- *
2054
- * Note: `$` is used to indicate that this interacts with the global scope
2055
- * @singleton Only one instance of each register is created per build, but thare can be more @@@
2056
- * @public exported from `@promptbook/core`
2057
- */
2058
- const $llmToolsRegister = new $Register('llm_execution_tools_constructors');
2059
- /**
2060
- * TODO: [ยฎ] DRY Register logic
2061
- */
2062
-
2063
- /**
2064
- * Path to the `.env` file which was used to configure LLM tools
2065
- *
2066
- * Note: `$` is used to indicate that this variable is changed by side effect in `$provideLlmToolsConfigurationFromEnv` through `$setUsedEnvFilename`
2067
- */
2068
- let $usedEnvFilename = null;
2630
+ }
2069
2631
  /**
2070
- * Pass the `.env` file which was used to configure LLM tools
2071
- *
2072
- * Note: `$` is used to indicate that this variable is making side effect
2073
- *
2074
- * @private internal log of `$provideLlmToolsConfigurationFromEnv` and `$registeredLlmToolsMessage`
2632
+ * TODO: [๐Ÿง ][๐Ÿ’ธ] Maybe make some common abstraction `interceptLlmTools` and use here (or use javascript Proxy?)
2633
+ * TODO: [๐Ÿง ] Is there some meaningfull way how to test this util
2634
+ * TODO: [๐Ÿง ][๐ŸŒฏ] Maybe a way how to hide ability to `get totalUsage`
2635
+ * > const [llmToolsWithUsage,getUsage] = countTotalUsage(llmTools);
2636
+ * TODO: [๐Ÿ‘ทโ€โ™‚๏ธ] @@@ Manual about construction of llmTools
2075
2637
  */
2076
- function $setUsedEnvFilename(filepath) {
2077
- $usedEnvFilename = filepath;
2078
- }
2638
+
2079
2639
  /**
2080
- * Creates a message with all registered LLM tools
2081
- *
2082
- * 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.
2083
2641
  *
2084
- * @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`
2085
2644
  */
2086
- function $registeredLlmToolsMessage() {
2087
- let env;
2088
- if ($isRunningInNode()) {
2089
- env = process.env;
2090
- // <- TODO: [โš›] Some DRY way how to get to `process.env` and pass it into functions - ACRY search for `env`
2091
- }
2092
- else {
2093
- env = {};
2094
- }
2095
- /**
2096
- * Mixes registered LLM tools from $llmToolsMetadataRegister and $llmToolsRegister
2097
- */
2098
- const all = [];
2099
- for (const { title, packageName, className, envVariables } of $llmToolsMetadataRegister.list()) {
2100
- if (all.some((item) => item.packageName === packageName && item.className === className)) {
2101
- continue;
2102
- }
2103
- all.push({ title, packageName, className, envVariables });
2104
- }
2105
- for (const { packageName, className } of $llmToolsRegister.list()) {
2106
- if (all.some((item) => item.packageName === packageName && item.className === className)) {
2107
- continue;
2108
- }
2109
- all.push({ packageName, className });
2645
+ function isRootPath(value) {
2646
+ if (value === '/') {
2647
+ return true;
2110
2648
  }
2111
- const metadata = all.map((metadata) => {
2112
- var _a, _b;
2113
- const isMetadataAviailable = $llmToolsMetadataRegister
2114
- .list()
2115
- .find(({ packageName, className }) => metadata.packageName === packageName && metadata.className === className);
2116
- const isInstalled = $llmToolsRegister
2117
- .list()
2118
- .find(({ packageName, className }) => metadata.packageName === packageName && metadata.className === className);
2119
- const isFullyConfigured = ((_a = metadata.envVariables) === null || _a === void 0 ? void 0 : _a.every((envVariableName) => env[envVariableName] !== undefined)) || false;
2120
- const isPartiallyConfigured = ((_b = metadata.envVariables) === null || _b === void 0 ? void 0 : _b.some((envVariableName) => env[envVariableName] !== undefined)) || false;
2121
- // <- Note: [๐Ÿ—จ]
2122
- return { ...metadata, isMetadataAviailable, isInstalled, isFullyConfigured, isPartiallyConfigured };
2123
- });
2124
- const usedEnvMessage = $usedEnvFilename === null ? `Unknown \`.env\` file` : `Used \`.env\` file:\n${$usedEnvFilename}`;
2125
- if (metadata.length === 0) {
2126
- return spaceTrim((block) => `
2127
- No LLM providers are available.
2128
-
2129
- ${block(usedEnvMessage)}
2130
- `);
2649
+ if (/^[A-Z]:\\$/i.test(value)) {
2650
+ return true;
2131
2651
  }
2132
- return spaceTrim((block) => `
2133
-
2134
- ${block(usedEnvMessage)}
2135
-
2136
- Relevant environment variables:
2137
- ${block(Object.keys(env)
2138
- .filter((envVariableName) => metadata.some(({ envVariables }) => envVariables === null || envVariables === void 0 ? void 0 : envVariables.includes(envVariableName)))
2139
- .map((envVariableName) => `- \`${envVariableName}\``)
2140
- .join('\n'))}
2141
-
2142
- Available LLM providers are:
2143
- ${block(metadata
2144
- .map(({ title, packageName, className, envVariables, isMetadataAviailable, isInstalled, isFullyConfigured, isPartiallyConfigured, }, i) => {
2145
- const morePieces = [];
2146
- if (just(false)) ;
2147
- else if (!isMetadataAviailable && !isInstalled) {
2148
- // TODO: [๏ฟฝ][๏ฟฝ] Maybe do allow to do auto-install if package not registered and not found
2149
- morePieces.push(`Not installed and no metadata, looks like a unexpected behavior`);
2150
- }
2151
- else if (isMetadataAviailable && !isInstalled) {
2152
- // TODO: [๏ฟฝ][๏ฟฝ]
2153
- morePieces.push(`Not installed`);
2154
- }
2155
- else if (!isMetadataAviailable && isInstalled) {
2156
- morePieces.push(`No metadata but installed, looks like a unexpected behavior`);
2157
- }
2158
- else if (isMetadataAviailable && isInstalled) {
2159
- morePieces.push(`Installed`);
2160
- }
2161
- else {
2162
- morePieces.push(`unknown state, looks like a unexpected behavior`);
2163
- } /* not else */
2164
- if (isFullyConfigured) {
2165
- morePieces.push(`Configured`);
2166
- }
2167
- else if (isPartiallyConfigured) {
2168
- morePieces.push(`Partially confugured, missing ${envVariables === null || envVariables === void 0 ? void 0 : envVariables.filter((envVariable) => env[envVariable] === undefined).join(' + ')}`);
2169
- }
2170
- else {
2171
- if (envVariables !== null) {
2172
- morePieces.push(`Not configured, to configure set env ${envVariables === null || envVariables === void 0 ? void 0 : envVariables.join(' + ')}`);
2173
- }
2174
- else {
2175
- morePieces.push(`Not configured`); // <- Note: Can not be configured via environment variables
2176
- }
2177
- }
2178
- let providerMessage = spaceTrim(`
2179
- ${i + 1}) **${title}** \`${className}\` from \`${packageName}\`
2180
- ${morePieces.join('; ')}
2181
- `);
2182
- if ($isRunningInNode) {
2183
- if (isInstalled && isFullyConfigured) {
2184
- providerMessage = colors.green(providerMessage);
2185
- }
2186
- else if (isInstalled && isPartiallyConfigured) {
2187
- providerMessage = colors.yellow(providerMessage);
2188
- }
2189
- else {
2190
- providerMessage = colors.gray(providerMessage);
2191
- }
2192
- }
2193
- return providerMessage;
2194
- })
2195
- .join('\n'))}
2196
- `);
2652
+ return false;
2197
2653
  }
2198
2654
  /**
2199
- * TODO: [ยฎ] DRY Register logic
2200
- * TODO: [๐Ÿง ][โš›] Maybe pass env as argument
2655
+ * TODO: [๐Ÿ] Make for MacOS paths
2201
2656
  */
2202
2657
 
2203
2658
  /**
@@ -2560,10 +3015,33 @@ async function $provideLlmToolsForWizzardOrCli(options) {
2560
3015
  if (!$isRunningInNode()) {
2561
3016
  throw new EnvironmentMismatchError('Function `$provideLlmToolsForWizzardOrCli` works only in Node.js environment');
2562
3017
  }
2563
- 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
+ }
2564
3041
  return cacheLlmTools(countUsage(
3042
+ // <- TODO: [๐ŸŒฏ] We dont use countUsage at all, maybe just unwrap it
2565
3043
  // <- Note: for example here we don`t want the [๐ŸŒฏ]
2566
- await $provideLlmToolsFromEnv()), {
3044
+ llmExecutionTools), {
2567
3045
  storage: new FileCacheStorage({ fs: $provideFilesystemForNode() }, {
2568
3046
  rootFolderPath: join(process.cwd(), DEFAULT_EXECUTION_CACHE_DIRNAME),
2569
3047
  }),
@@ -2579,31 +3057,88 @@ async function $provideLlmToolsForWizzardOrCli(options) {
2579
3057
  */
2580
3058
 
2581
3059
  /**
2582
- * Just says that the variable is not used but should be kept
2583
- * No side effects.
2584
- *
2585
- * Note: It can be usefull for:
2586
- *
2587
- * 1) Suppressing eager optimization of unused imports
2588
- * 2) Suppressing eslint errors of unused variables in the tests
2589
- * 3) Keeping the type of the variable for type testing
2590
- *
2591
- * @param value any values
2592
- * @returns void
2593
- * @private within the repository
2594
- */
2595
- function keepUnused(...valuesToKeep) {
2596
- }
2597
-
2598
- /**
2599
- * Just says that the variable is not used directlys but should be kept because the existence of the variable is important
2600
- *
2601
- * @param value any values
2602
- * @returns void
2603
- * @private within the repository
3060
+ * @private utility of CLI
2604
3061
  */
2605
- function $sideEffect(...sideEffectSubjects) {
2606
- 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
+ }
2607
3142
  }
2608
3143
 
2609
3144
  /**
@@ -2620,8 +3155,10 @@ function $initializeListModelsCommand(program) {
2620
3155
  `));
2621
3156
  listModelsCommand.alias('models');
2622
3157
  listModelsCommand.alias('llm');
2623
- listModelsCommand.action(handleActionErrors(async () => {
2624
- 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 });
2625
3162
  $sideEffect(llm);
2626
3163
  // <- Note: Providing LLM tools will make a side effect of registering all available LLM tools to show the message
2627
3164
  console.info($registeredLlmToolsMessage());
@@ -3181,49 +3718,73 @@ function $initializeListScrapersCommand(program) {
3181
3718
  */
3182
3719
 
3183
3720
  /**
3184
- * Converts PipelineCollection to serialized JSON
3721
+ * Initializes `login` command for Promptbook CLI utilities
3185
3722
  *
3186
- * 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
3187
3724
  *
3188
- * @public exported from `@promptbook/core`
3189
- */
3190
- async function collectionToJson(collection) {
3191
- const pipelineUrls = await collection.listPipelines();
3192
- const promptbooks = await Promise.all(pipelineUrls.map((url) => collection.getPipelineByUrl(url)));
3193
- return promptbooks;
3194
- }
3195
- /**
3196
- * TODO: [๐Ÿง ] Maybe clear `sourceFile` or clear when exposing through API or remote server
3725
+ * @private internal function of `promptbookCli`
3197
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.
3198
3757
 
3199
- /**
3200
- * This error indicates that the promptbook in a markdown format cannot be parsed into a valid promptbook object
3201
- *
3202
- * @public exported from `@promptbook/core`
3203
- */
3204
- class ParseError extends Error {
3205
- constructor(message) {
3206
- super(message);
3207
- this.name = 'ParseError';
3208
- Object.setPrototypeOf(this, ParseError.prototype);
3209
- }
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
+ }));
3210
3765
  }
3211
3766
  /**
3212
- * 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`
3213
3771
  */
3214
3772
 
3215
3773
  /**
3216
- * 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
3217
3777
  *
3218
3778
  * @public exported from `@promptbook/core`
3219
3779
  */
3220
- class PipelineLogicError extends Error {
3221
- constructor(message) {
3222
- super(message);
3223
- this.name = 'PipelineLogicError';
3224
- Object.setPrototypeOf(this, PipelineLogicError.prototype);
3225
- }
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;
3226
3784
  }
3785
+ /**
3786
+ * TODO: [๐Ÿง ] Maybe clear `sourceFile` or clear when exposing through API or remote server
3787
+ */
3227
3788
 
3228
3789
  /**
3229
3790
  * Tests if given string is valid semantic version
@@ -3621,21 +4182,6 @@ async function loadArchive(filePath, fs) {
3621
4182
 
3622
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"}];
3623
4184
 
3624
- /**
3625
- * Checks if value is valid email
3626
- *
3627
- * @public exported from `@promptbook/utils`
3628
- */
3629
- function isValidEmail(email) {
3630
- if (typeof email !== 'string') {
3631
- return false;
3632
- }
3633
- if (email.split('\n').length > 1) {
3634
- return false;
3635
- }
3636
- return /^.+@.+\..+$/.test(email);
3637
- }
3638
-
3639
4185
  /**
3640
4186
  * Function isValidJsonString will tell you if the string is valid JSON or not
3641
4187
  *
@@ -3869,32 +4415,6 @@ function taskParameterJsonToString(taskParameterJson) {
3869
4415
  * TODO: [๐Ÿง ] Should be in generated .book.md file GENERATOR_WARNING
3870
4416
  */
3871
4417
 
3872
- /**
3873
- * This error indicates that promptbook not found in the collection
3874
- *
3875
- * @public exported from `@promptbook/core`
3876
- */
3877
- class NotFoundError extends Error {
3878
- constructor(message) {
3879
- super(message);
3880
- this.name = 'NotFoundError';
3881
- Object.setPrototypeOf(this, NotFoundError.prototype);
3882
- }
3883
- }
3884
-
3885
- /**
3886
- * This error indicates errors in referencing promptbooks between each other
3887
- *
3888
- * @public exported from `@promptbook/core`
3889
- */
3890
- class PipelineUrlError extends Error {
3891
- constructor(message) {
3892
- super(message);
3893
- this.name = 'PipelineUrlError';
3894
- Object.setPrototypeOf(this, PipelineUrlError.prototype);
3895
- }
3896
- }
3897
-
3898
4418
  /**
3899
4419
  * Parses the task and returns the list of all parameter names
3900
4420
  *
@@ -4065,24 +4585,6 @@ function createCollectionFromJson(...promptbooks) {
4065
4585
  return new SimplePipelineCollection(...promptbooks);
4066
4586
  }
4067
4587
 
4068
- /**
4069
- * This error type indicates that some tools are missing for pipeline execution or preparation
4070
- *
4071
- * @public exported from `@promptbook/core`
4072
- */
4073
- class MissingToolsError extends Error {
4074
- constructor(message) {
4075
- super(spaceTrim$1((block) => `
4076
- ${block(message)}
4077
-
4078
- Note: You have probbably forgot to provide some tools for pipeline execution or preparation
4079
-
4080
- `));
4081
- this.name = 'MissingToolsError';
4082
- Object.setPrototypeOf(this, MissingToolsError.prototype);
4083
- }
4084
- }
4085
-
4086
4588
  /**
4087
4589
  * Determine if the pipeline is fully prepared
4088
4590
  *
@@ -4108,217 +4610,47 @@ function isPipelinePrepared(pipeline) {
4108
4610
  > return false;
4109
4611
  > }
4110
4612
  */
4111
- return true;
4112
- }
4113
- /**
4114
- * TODO: [๐Ÿ”ƒ][main] If the pipeline was prepared with different version or different set of models, prepare it once again
4115
- * TODO: [๐Ÿ ] Maybe base this on `makeValidator`
4116
- * TODO: [๐ŸงŠ] Pipeline can be partially prepared, this should return true ONLY if fully prepared
4117
- * TODO: [๐Ÿงฟ] Maybe do same process with same granularity and subfinctions as `preparePipeline`
4118
- * - [๐Ÿ] ? Is context in each task
4119
- * - [โ™จ] Are examples prepared
4120
- * - [โ™จ] Are tasks prepared
4121
- */
4122
-
4123
- /**
4124
- * Recursively converts JSON strings to JSON objects
4125
-
4126
- * @public exported from `@promptbook/utils`
4127
- */
4128
- function jsonStringsToJsons(object) {
4129
- if (object === null) {
4130
- return object;
4131
- }
4132
- if (Array.isArray(object)) {
4133
- return object.map(jsonStringsToJsons);
4134
- }
4135
- if (typeof object !== 'object') {
4136
- return object;
4137
- }
4138
- const newObject = { ...object };
4139
- for (const [key, value] of Object.entries(object)) {
4140
- if (typeof value === 'string' && isValidJsonString(value)) {
4141
- newObject[key] = JSON.parse(value);
4142
- }
4143
- else {
4144
- newObject[key] = jsonStringsToJsons(value);
4145
- }
4146
- }
4147
- return newObject;
4148
- }
4149
- /**
4150
- * TODO: Type the return type correctly
4151
- */
4152
-
4153
- /**
4154
- * This error indicates problems parsing the format value
4155
- *
4156
- * For example, when the format value is not a valid JSON or CSV
4157
- * This is not thrown directly but in extended classes
4158
- *
4159
- * @public exported from `@promptbook/core`
4160
- */
4161
- class AbstractFormatError extends Error {
4162
- // Note: To allow instanceof do not put here error `name`
4163
- // public readonly name = 'AbstractFormatError';
4164
- constructor(message) {
4165
- super(message);
4166
- Object.setPrototypeOf(this, AbstractFormatError.prototype);
4167
- }
4168
- }
4169
-
4170
- /**
4171
- * This error indicates problem with parsing of CSV
4172
- *
4173
- * @public exported from `@promptbook/core`
4174
- */
4175
- class CsvFormatError extends AbstractFormatError {
4176
- constructor(message) {
4177
- super(message);
4178
- this.name = 'CsvFormatError';
4179
- Object.setPrototypeOf(this, CsvFormatError.prototype);
4180
- }
4181
- }
4182
-
4183
- /**
4184
- * This error indicates that the pipeline collection cannot be propperly loaded
4185
- *
4186
- * @public exported from `@promptbook/core`
4187
- */
4188
- class CollectionError extends Error {
4189
- constructor(message) {
4190
- super(message);
4191
- this.name = 'CollectionError';
4192
- Object.setPrototypeOf(this, CollectionError.prototype);
4193
- }
4194
- }
4195
-
4196
- /**
4197
- * This error occurs when some expectation is not met in the execution of the pipeline
4198
- *
4199
- * @public exported from `@promptbook/core`
4200
- * Note: Do not throw this error, its reserved for `checkExpectations` and `createPipelineExecutor` and public ONLY to be serializable through remote server
4201
- * Note: Always thrown in `checkExpectations` and catched in `createPipelineExecutor` and rethrown as `PipelineExecutionError`
4202
- * Note: This is a kindof subtype of PipelineExecutionError
4203
- */
4204
- class ExpectError extends Error {
4205
- constructor(message) {
4206
- super(message);
4207
- this.name = 'ExpectError';
4208
- Object.setPrototypeOf(this, ExpectError.prototype);
4209
- }
4210
- }
4211
-
4212
- /**
4213
- * This error indicates that the promptbook can not retrieve knowledge from external sources
4214
- *
4215
- * @public exported from `@promptbook/core`
4216
- */
4217
- class KnowledgeScrapeError extends Error {
4218
- constructor(message) {
4219
- super(message);
4220
- this.name = 'KnowledgeScrapeError';
4221
- Object.setPrototypeOf(this, KnowledgeScrapeError.prototype);
4222
- }
4223
- }
4224
-
4225
- /**
4226
- * This error type indicates that some limit was reached
4227
- *
4228
- * @public exported from `@promptbook/core`
4229
- */
4230
- class LimitReachedError extends Error {
4231
- constructor(message) {
4232
- super(message);
4233
- this.name = 'LimitReachedError';
4234
- Object.setPrototypeOf(this, LimitReachedError.prototype);
4235
- }
4236
- }
4237
-
4238
- /**
4239
- * Index of all custom errors
4240
- *
4241
- * @public exported from `@promptbook/core`
4242
- */
4243
- const PROMPTBOOK_ERRORS = {
4244
- AbstractFormatError,
4245
- CsvFormatError,
4246
- CollectionError,
4247
- EnvironmentMismatchError,
4248
- ExpectError,
4249
- KnowledgeScrapeError,
4250
- LimitReachedError,
4251
- MissingToolsError,
4252
- NotFoundError,
4253
- NotYetImplementedError,
4254
- ParseError,
4255
- PipelineExecutionError,
4256
- PipelineLogicError,
4257
- PipelineUrlError,
4258
- UnexpectedError,
4259
- // TODO: [๐Ÿช‘]> VersionMismatchError,
4260
- };
4261
- /**
4262
- * Index of all javascript errors
4263
- *
4264
- * @private for internal usage
4265
- */
4266
- const COMMON_JAVASCRIPT_ERRORS = {
4267
- Error,
4268
- EvalError,
4269
- RangeError,
4270
- ReferenceError,
4271
- SyntaxError,
4272
- TypeError,
4273
- URIError,
4274
- AggregateError,
4275
- /*
4276
- Note: Not widely supported
4277
- > InternalError,
4278
- > ModuleError,
4279
- > HeapError,
4280
- > WebAssemblyCompileError,
4281
- > WebAssemblyRuntimeError,
4282
- */
4283
- };
4284
- /**
4285
- * Index of all errors
4286
- *
4287
- * @private for internal usage
4288
- */
4289
- const ALL_ERRORS = {
4290
- ...PROMPTBOOK_ERRORS,
4291
- ...COMMON_JAVASCRIPT_ERRORS,
4292
- };
4613
+ return true;
4614
+ }
4293
4615
  /**
4294
- * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
4616
+ * TODO: [๐Ÿ”ƒ][main] If the pipeline was prepared with different version or different set of models, prepare it once again
4617
+ * TODO: [๐Ÿ ] Maybe base this on `makeValidator`
4618
+ * TODO: [๐ŸงŠ] Pipeline can be partially prepared, this should return true ONLY if fully prepared
4619
+ * TODO: [๐Ÿงฟ] Maybe do same process with same granularity and subfinctions as `preparePipeline`
4620
+ * - [๐Ÿ] ? Is context in each task
4621
+ * - [โ™จ] Are examples prepared
4622
+ * - [โ™จ] Are tasks prepared
4295
4623
  */
4296
4624
 
4297
4625
  /**
4298
- * Deserializes the error object
4299
- *
4626
+ * Recursively converts JSON strings to JSON objects
4627
+
4300
4628
  * @public exported from `@promptbook/utils`
4301
4629
  */
4302
- function deserializeError(error) {
4303
- const { name, stack, id } = error; // Added id
4304
- let { message } = error;
4305
- let ErrorClass = ALL_ERRORS[error.name];
4306
- if (ErrorClass === undefined) {
4307
- ErrorClass = Error;
4308
- message = `${name}: ${message}`;
4630
+ function jsonStringsToJsons(object) {
4631
+ if (object === null) {
4632
+ return object;
4309
4633
  }
4310
- if (stack !== undefined && stack !== '') {
4311
- message = spaceTrim((block) => `
4312
- ${block(message)}
4313
-
4314
- Original stack trace:
4315
- ${block(stack || '')}
4316
- `);
4634
+ if (Array.isArray(object)) {
4635
+ return object.map(jsonStringsToJsons);
4317
4636
  }
4318
- const deserializedError = new ErrorClass(message);
4319
- deserializedError.id = id; // Assign id to the error object
4320
- return deserializedError;
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;
4321
4650
  }
4651
+ /**
4652
+ * TODO: Type the return type correctly
4653
+ */
4322
4654
 
4323
4655
  /**
4324
4656
  * Asserts that the execution of a Promptbook is successful
@@ -4471,6 +4803,10 @@ function serializeError(error) {
4471
4803
 
4472
4804
  Cannot serialize error with name "${name}"
4473
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
+
4474
4810
  ${block(stack || message)}
4475
4811
 
4476
4812
  `));
@@ -11381,7 +11717,6 @@ function $initializeMakeCommand(program) {
11381
11717
  makeCommand.option('--no-validation', `Do not validate logic of pipelines in collection`, true);
11382
11718
  makeCommand.option('--validation', `Types of validations separated by comma (options "logic","imports")`, 'logic,imports');
11383
11719
  makeCommand.option('-r, --reload', `Call LLM models even if same prompt with result is in the cache`, false);
11384
- makeCommand.option('-v, --verbose', `Is output verbose`, false);
11385
11720
  makeCommand.option('-o, --output <path>', spaceTrim(`
11386
11721
  Where to save the builded collection
11387
11722
 
@@ -11395,7 +11730,8 @@ function $initializeMakeCommand(program) {
11395
11730
  Note: This can be used only with "javascript" or "typescript" format
11396
11731
 
11397
11732
  `), DEFAULT_GET_PIPELINE_COLLECTION_FUNCTION_NAME);
11398
- 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;
11399
11735
  if (!isValidJavascriptName(functionName)) {
11400
11736
  console.error(colors.red(`Function name "${functionName}" is not valid javascript name`));
11401
11737
  return process.exit(1);
@@ -11423,7 +11759,10 @@ function $initializeMakeCommand(program) {
11423
11759
  isCacheReloaded,
11424
11760
  }; /* <- TODO: ` satisfies PrepareAndScrapeOptions` */
11425
11761
  const fs = $provideFilesystemForNode(prepareAndScrapeOptions);
11426
- const llm = await $provideLlmToolsForWizzardOrCli(prepareAndScrapeOptions);
11762
+ const llm = await $provideLlmToolsForCli({
11763
+ cliOptions,
11764
+ ...prepareAndScrapeOptions,
11765
+ });
11427
11766
  const executables = await $provideExecutablesForNode(prepareAndScrapeOptions);
11428
11767
  const tools = {
11429
11768
  llm,
@@ -11691,8 +12030,8 @@ function $initializePrettifyCommand(program) {
11691
12030
  // <- TODO: [๐ŸงŸโ€โ™‚๏ธ] Unite path to promptbook collection argument
11692
12031
  'Pipelines to prettify as glob pattern');
11693
12032
  prettifyCommand.option('-i, --ignore <glob>', `Ignore as glob pattern`);
11694
- prettifyCommand.option('-v, --verbose', `Is output verbose`, false);
11695
- prettifyCommand.action(handleActionErrors(async (filesGlob, { ignore, verbose: isVerbose }) => {
12033
+ prettifyCommand.action(handleActionErrors(async (filesGlob, cliOptions) => {
12034
+ const { ignore, verbose: isVerbose } = cliOptions;
11696
12035
  const filenames = await glob(filesGlob, { ignore });
11697
12036
  // <- TODO: [๐Ÿ˜ถ]
11698
12037
  for (const filename of filenames) {
@@ -12293,13 +12632,12 @@ function $initializeRunCommand(program) {
12293
12632
  // TODO: [๐Ÿง…] DRY command arguments
12294
12633
  runCommand.argument('[pipelineSource]', 'Path to book file OR URL to book file, if not provided it will be asked');
12295
12634
  runCommand.option('-r, --reload', `Call LLM models even if same prompt with result is in the cache`, false);
12296
- runCommand.option('-v, --verbose', `Is output verbose`, false);
12297
- runCommand.option('--no-interactive', `Input is not interactive, if true you need to pass all the input parameters through --json`);
12298
12635
  runCommand.option('--no-formfactor', `When set, behavior of the interactive mode is not changed by the formfactor of the pipeline`);
12299
12636
  runCommand.option('-j, --json <json>', `Pass all or some input parameters as JSON record, if used the output is also returned as JSON`);
12300
12637
  runCommand.option('-s, --save-report <path>', `Save report to file`);
12301
- runCommand.action(handleActionErrors(async (pipelineSource, options) => {
12302
- 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;
12303
12641
  if (pipelineSource.includes('-') && normalizeToKebabCase(pipelineSource) === pipelineSource) {
12304
12642
  console.error(colors.red(`""${pipelineSource}" is not a valid command or book. See 'ptbk --help'.`));
12305
12643
  return process.exit(1);
@@ -12324,7 +12662,7 @@ function $initializeRunCommand(program) {
12324
12662
  const fs = $provideFilesystemForNode(prepareAndScrapeOptions);
12325
12663
  let llm;
12326
12664
  try {
12327
- llm = await $provideLlmToolsForWizzardOrCli(prepareAndScrapeOptions);
12665
+ llm = await $provideLlmToolsForCli({ cliOptions, ...prepareAndScrapeOptions });
12328
12666
  }
12329
12667
  catch (error) {
12330
12668
  if (!(error instanceof Error)) {
@@ -12381,7 +12719,7 @@ function $initializeRunCommand(program) {
12381
12719
  fs,
12382
12720
  fetch: scraperFetch,
12383
12721
  scrapers: await $provideScrapersForNode({ fs, llm, executables }, prepareAndScrapeOptions),
12384
- script: [new JavascriptExecutionTools(options)],
12722
+ script: [new JavascriptExecutionTools(cliOptions)],
12385
12723
  };
12386
12724
  if (isVerbose) {
12387
12725
  console.info(colors.gray('--- Getting the book ---'));
@@ -12545,11 +12883,12 @@ function $initializeRunCommand(program) {
12545
12883
  * @public exported from `@promptbook/remote-server`
12546
12884
  */
12547
12885
  function startRemoteServer(options) {
12548
- const { port, collection, createLlmExecutionTools, isAnonymousModeAllowed, isApplicationModeAllowed, isVerbose = DEFAULT_IS_VERBOSE, } = {
12886
+ const { port, collection, createLlmExecutionTools, isAnonymousModeAllowed, isApplicationModeAllowed, isVerbose = DEFAULT_IS_VERBOSE, login, } = {
12549
12887
  isAnonymousModeAllowed: false,
12550
12888
  isApplicationModeAllowed: false,
12551
12889
  collection: null,
12552
12890
  createLlmExecutionTools: null,
12891
+ login: null,
12553
12892
  ...options,
12554
12893
  };
12555
12894
  // <- TODO: [๐Ÿฆช] Some helper type to be able to use discriminant union types with destructuring
@@ -12627,13 +12966,14 @@ function startRemoteServer(options) {
12627
12966
  servers: [
12628
12967
  {
12629
12968
  url: `http://localhost:${port}${rootPath}`,
12969
+ // <- TODO: !!!!! Probbably: Pass `remoteServerUrl` instead of `port` and `rootPath`
12630
12970
  },
12631
12971
  ],
12632
12972
  },
12633
12973
  apis: ['./src/remote-server/**/*.ts'], // Adjust path as needed
12634
12974
  };
12635
12975
  const swaggerSpec = swaggerJsdoc(swaggerOptions);
12636
- app.use(`${rootPath}/api-docs`, swaggerUi.serve, swaggerUi.setup(swaggerSpec));
12976
+ app.use([`/api-docs`, `${rootPath}/api-docs`], swaggerUi.serve, swaggerUi.setup(swaggerSpec));
12637
12977
  const runningExecutionTasks = [];
12638
12978
  // <- TODO: [๐Ÿคฌ] Identify the users
12639
12979
  // TODO: [๐Ÿง ] Do here some garbage collection of finished tasks
@@ -12683,9 +13023,12 @@ function startRemoteServer(options) {
12683
13023
 
12684
13024
  ## Paths
12685
13025
 
12686
- ${block(app._router.stack
12687
- .map(({ route }) => (route === null || route === void 0 ? void 0 : route.path) || null)
12688
- .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
+ ]
12689
13032
  .map((path) => `- ${path}`)
12690
13033
  .join('\n'))}
12691
13034
 
@@ -12703,7 +13046,64 @@ function startRemoteServer(options) {
12703
13046
  https://github.com/webgptorg/promptbook
12704
13047
  `));
12705
13048
  });
12706
- // TODO: !!!!!! Add login route
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
+ });
12707
13107
  /**
12708
13108
  * @swagger
12709
13109
  * /books:
@@ -12720,7 +13120,7 @@ function startRemoteServer(options) {
12720
13120
  * items:
12721
13121
  * type: string
12722
13122
  */
12723
- app.get(`${rootPath}/books`, async (request, response) => {
13123
+ app.get([`/books`, `${rootPath}/books`], async (request, response) => {
12724
13124
  if (collection === null) {
12725
13125
  response.status(500).send('No collection available');
12726
13126
  return;
@@ -12753,7 +13153,7 @@ function startRemoteServer(options) {
12753
13153
  * 404:
12754
13154
  * description: Book not found.
12755
13155
  */
12756
- app.get(`${rootPath}/books/*`, async (request, response) => {
13156
+ app.get([`/books/*`, `${rootPath}/books/*`], async (request, response) => {
12757
13157
  try {
12758
13158
  if (collection === null) {
12759
13159
  response.status(500).send('No collection nor books available');
@@ -12823,10 +13223,10 @@ function startRemoteServer(options) {
12823
13223
  * items:
12824
13224
  * type: object
12825
13225
  */
12826
- app.get(`${rootPath}/executions`, async (request, response) => {
13226
+ app.get([`/executions`, `${rootPath}/executions`], async (request, response) => {
12827
13227
  response.send(runningExecutionTasks.map((runningExecutionTask) => exportExecutionTask(runningExecutionTask, false)));
12828
13228
  });
12829
- app.get(`${rootPath}/executions/last`, async (request, response) => {
13229
+ app.get([`/executions/last`, `${rootPath}/executions/last`], async (request, response) => {
12830
13230
  // TODO: [๐Ÿคฌ] Filter only for user
12831
13231
  if (runningExecutionTasks.length === 0) {
12832
13232
  response.status(404).send('No execution tasks found');
@@ -12835,7 +13235,7 @@ function startRemoteServer(options) {
12835
13235
  const lastExecutionTask = runningExecutionTasks[runningExecutionTasks.length - 1];
12836
13236
  response.send(exportExecutionTask(lastExecutionTask, true));
12837
13237
  });
12838
- app.get(`${rootPath}/executions/:taskId`, async (request, response) => {
13238
+ app.get([`/executions/:taskId`, `${rootPath}/executions/:taskId`], async (request, response) => {
12839
13239
  const { taskId } = request.params;
12840
13240
  // TODO: [๐Ÿคฌ] Filter only for user
12841
13241
  const executionTask = runningExecutionTasks.find((executionTask) => executionTask.taskId === taskId);
@@ -12876,7 +13276,7 @@ function startRemoteServer(options) {
12876
13276
  * 400:
12877
13277
  * description: Invalid input.
12878
13278
  */
12879
- app.post(`${rootPath}/executions/new`, async (request, response) => {
13279
+ app.post([`/executions/new`, `${rootPath}/executions/new`], async (request, response) => {
12880
13280
  try {
12881
13281
  const { inputParameters, identification /* <- [๐Ÿคฌ] */ } = request.body;
12882
13282
  const pipelineUrl = request.body.pipelineUrl || request.body.book;
@@ -13112,12 +13512,12 @@ function $initializeStartServerCommand(program) {
13112
13512
  `));
13113
13513
  startServerCommand.option('--allow-anonymous', `Is anonymous mode allowed`, false);
13114
13514
  startServerCommand.option('-r, --reload', `Call LLM models even if same prompt with result is in the cache`, false);
13115
- startServerCommand.option('-v, --verbose', `Is output verbose`, false);
13116
13515
  startServerCommand.description(spaceTrim(`
13117
13516
  Starts a remote server to execute books
13118
13517
  `));
13119
13518
  startServerCommand.alias('server');
13120
- 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;
13121
13521
  if (rawUrl && !isValidUrl(rawUrl)) {
13122
13522
  console.error(colors.red(`Invalid URL: ${rawUrl}`));
13123
13523
  return process.exit(1);
@@ -13146,7 +13546,7 @@ function $initializeStartServerCommand(program) {
13146
13546
  isCacheReloaded,
13147
13547
  }; /* <- TODO: ` satisfies PrepareAndScrapeOptions` */
13148
13548
  const fs = $provideFilesystemForNode(prepareAndScrapeOptions);
13149
- const llm = await $provideLlmToolsForWizzardOrCli(prepareAndScrapeOptions);
13549
+ const llm = await $provideLlmToolsForCli({ cliOptions, ...prepareAndScrapeOptions });
13150
13550
  const executables = await $provideExecutablesForNode(prepareAndScrapeOptions);
13151
13551
  const tools = {
13152
13552
  llm,
@@ -13170,6 +13570,9 @@ function $initializeStartServerCommand(program) {
13170
13570
  isAnonymousModeAllowed,
13171
13571
  isApplicationModeAllowed: true,
13172
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
+ },
13173
13576
  createLlmExecutionTools(options) {
13174
13577
  const { appId, userId } = options;
13175
13578
  TODO_USE({ appId, userId });
@@ -13206,8 +13609,8 @@ function $initializeTestCommand(program) {
13206
13609
  testCommand.option('--no-validation', `Do not validate logic of pipelines in collection`, true);
13207
13610
  testCommand.option('--no-prepare', `Do not prepare the pipelines, ideal when no LLM tools or scrapers available`, true);
13208
13611
  testCommand.option('-r, --reload', `Call LLM models even if same prompt with result is in the cache `, false);
13209
- testCommand.option('-v, --verbose', `Is output verbose`, false);
13210
- 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;
13211
13614
  let tools = undefined;
13212
13615
  if (isPrepared) {
13213
13616
  // TODO: DRY [โ—ฝ]
@@ -13216,7 +13619,7 @@ function $initializeTestCommand(program) {
13216
13619
  isCacheReloaded,
13217
13620
  }; /* <- TODO: ` satisfies PrepareAndScrapeOptions` */
13218
13621
  const fs = $provideFilesystemForNode(prepareAndScrapeOptions);
13219
- const llm = await $provideLlmToolsForWizzardOrCli(prepareAndScrapeOptions);
13622
+ const llm = await $provideLlmToolsForCli({ cliOptions, ...prepareAndScrapeOptions });
13220
13623
  const executables = await $provideExecutablesForNode(prepareAndScrapeOptions);
13221
13624
  tools = {
13222
13625
  llm,
@@ -13277,56 +13680,16 @@ function $initializeTestCommand(program) {
13277
13680
  */
13278
13681
 
13279
13682
  /**
13280
- * Initializes `login` command for Promptbook CLI utilities
13281
- *
13282
- * 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
13283
13684
  *
13284
- * @private internal function of `promptbookCli`
13685
+ * @private utility of CLI
13285
13686
  */
13286
- function $initializeLoginCommand(program) {
13287
- const loginCommand = program.command('login');
13288
- loginCommand.description(spaceTrim(`
13289
- Login to the remote Promptbook server
13290
- `));
13291
- loginCommand.action(handleActionErrors(async () => {
13292
- // @@@
13293
- console.error(colors.green(spaceTrim(`
13294
- You will be logged in to https://promptbook.studio server.
13295
- If you don't have an account, it will be created automatically.
13296
- `)));
13297
- const { email, password } = await prompts([
13298
- {
13299
- type: 'text',
13300
- name: 'email',
13301
- message: 'Enter your email:',
13302
- validate: (value) => (isValidEmail(value) ? true : 'Valid email is required'),
13303
- },
13304
- {
13305
- type: 'password',
13306
- name: 'password',
13307
- message: 'Enter your password:',
13308
- validate: (value) => value.length /* <- TODO: [๐Ÿง ] Better password validation */ > 0 ? true : 'Password is required',
13309
- },
13310
- ]);
13311
- TODO_USE(email, password);
13312
- await forTime(1000);
13313
- console.error(colors.green(spaceTrim(`
13314
- Your account ${email} was successfully created.
13315
-
13316
- Please verify your email:
13317
- https://brj.app/api/v1/customer/register-account?apiKey=PRODdh003eNKaec7PoO1AzU244tsL4WO
13318
-
13319
- After verification, you will receive 500 000 credits for free ๐ŸŽ‰
13320
- `)));
13321
- return process.exit(0);
13322
- }));
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);
13323
13692
  }
13324
- /**
13325
- * TODO: Pass remote server URL (and path)
13326
- * TODO: Implement non-interactive login
13327
- * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
13328
- * Note: [๐ŸŸก] Code in this file should never be published outside of `@promptbook/cli`
13329
- */
13330
13693
 
13331
13694
  /**
13332
13695
  * Runs CLI utilities of Promptbook package
@@ -13352,6 +13715,7 @@ async function promptbookCli() {
13352
13715
  program.alias('ptbk');
13353
13716
  program.version(PROMPTBOOK_ENGINE_VERSION);
13354
13717
  program.description(CLAIM);
13718
+ // Note: Theese options are valid for all commands
13355
13719
  $initializeAboutCommand(program);
13356
13720
  $initializeRunCommand(program);
13357
13721
  $initializeLoginCommand(program);
@@ -13362,6 +13726,8 @@ async function promptbookCli() {
13362
13726
  $initializeListModelsCommand(program);
13363
13727
  $initializeListScrapersCommand(program);
13364
13728
  $initializeStartServerCommand(program);
13729
+ // TODO: [๐Ÿง ] Should it be here or not> $addGlobalOptionsToCommand(program);
13730
+ program.commands.forEach($addGlobalOptionsToCommand);
13365
13731
  program.parse(process.argv);
13366
13732
  }
13367
13733
  /**
@@ -13411,8 +13777,7 @@ const _AnthropicClaudeMetadataRegistration = $llmToolsMetadataRegister.register(
13411
13777
  options: {
13412
13778
  apiKey: 'sk-ant-api03-',
13413
13779
  isProxied: true,
13414
- remoteUrl: DEFAULT_REMOTE_URL,
13415
- path: DEFAULT_REMOTE_URL_PATH,
13780
+ remoteServerUrl: DEFAULT_REMOTE_SERVER_URL,
13416
13781
  },
13417
13782
  };
13418
13783
  },
@@ -13435,146 +13800,6 @@ const _AnthropicClaudeMetadataRegistration = $llmToolsMetadataRegister.register(
13435
13800
  * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
13436
13801
  */
13437
13802
 
13438
- /**
13439
- * Creates a connection to the remote proxy server.
13440
- *
13441
- * Note: This function creates a connection to the remote server and returns a socket but responsibility of closing the connection is on the caller
13442
- *
13443
- * @private internal utility function
13444
- */
13445
- async function createRemoteClient(options) {
13446
- const { remoteUrl, path } = options;
13447
- return new Promise((resolve, reject) => {
13448
- const socket = io(remoteUrl, {
13449
- retries: CONNECTION_RETRIES_LIMIT,
13450
- timeout: CONNECTION_TIMEOUT_MS,
13451
- path,
13452
- // path: `${this.remoteUrl.pathname}/socket.io`,
13453
- transports: [/*'websocket', <- TODO: [๐ŸŒฌ] Make websocket transport work */ 'polling'],
13454
- });
13455
- // console.log('Connecting to', this.options.remoteUrl.href, { socket });
13456
- socket.on('connect', () => {
13457
- resolve(socket);
13458
- });
13459
- // TODO: [๐Ÿ’ฉ] Better timeout handling
13460
- setTimeout(() => {
13461
- reject(new Error(`Timeout while connecting to ${remoteUrl}`));
13462
- }, CONNECTION_TIMEOUT_MS);
13463
- });
13464
- }
13465
-
13466
- /**
13467
- * Remote server is a proxy server that uses its execution tools internally and exposes the executor interface externally.
13468
- *
13469
- * You can simply use `RemoteExecutionTools` on client-side javascript and connect to your remote server.
13470
- * This is useful to make all logic on browser side but not expose your API keys or no need to use customer's GPU.
13471
- *
13472
- * @see https://github.com/webgptorg/promptbook#remote-server
13473
- * @public exported from `@promptbook/remote-client`
13474
- */
13475
- class RemoteLlmExecutionTools {
13476
- /* <- TODO: [๐Ÿš] `, Destroyable` */
13477
- constructor(options) {
13478
- this.options = options;
13479
- }
13480
- get title() {
13481
- // TODO: [๐Ÿง ] Maybe fetch title+description from the remote server (as well as if model methods are defined)
13482
- return 'Remote server';
13483
- }
13484
- get description() {
13485
- return 'Use all models by your remote server';
13486
- }
13487
- /**
13488
- * Check the configuration of all execution tools
13489
- */
13490
- async checkConfiguration() {
13491
- const socket = await createRemoteClient(this.options);
13492
- socket.disconnect();
13493
- // TODO: [main] !!3 Check version of the remote server and compatibility
13494
- // TODO: [๐ŸŽ] Send checkConfiguration
13495
- }
13496
- /**
13497
- * List all available models that can be used
13498
- */
13499
- async listModels() {
13500
- // TODO: [๐Ÿ‘’] Listing models (and checking configuration) probbably should go through REST API not Socket.io
13501
- const socket = await createRemoteClient(this.options);
13502
- socket.emit('listModels-request', {
13503
- identification: this.options.identification,
13504
- } /* <- Note: [๐Ÿค›] */);
13505
- const promptResult = await new Promise((resolve, reject) => {
13506
- socket.on('listModels-response', (response) => {
13507
- resolve(response.models);
13508
- socket.disconnect();
13509
- });
13510
- socket.on('error', (error) => {
13511
- reject(deserializeError(error));
13512
- socket.disconnect();
13513
- });
13514
- });
13515
- socket.disconnect();
13516
- return promptResult;
13517
- }
13518
- /**
13519
- * Calls remote proxy server to use a chat model
13520
- */
13521
- callChatModel(prompt) {
13522
- if (this.options.isVerbose) {
13523
- console.info(`๐Ÿ–‹ Remote callChatModel call`);
13524
- }
13525
- return /* not await */ this.callCommonModel(prompt);
13526
- }
13527
- /**
13528
- * Calls remote proxy server to use a completion model
13529
- */
13530
- callCompletionModel(prompt) {
13531
- if (this.options.isVerbose) {
13532
- console.info(`๐Ÿ’ฌ Remote callCompletionModel call`);
13533
- }
13534
- return /* not await */ this.callCommonModel(prompt);
13535
- }
13536
- /**
13537
- * Calls remote proxy server to use a embedding model
13538
- */
13539
- callEmbeddingModel(prompt) {
13540
- if (this.options.isVerbose) {
13541
- console.info(`๐Ÿ’ฌ Remote callEmbeddingModel call`);
13542
- }
13543
- return /* not await */ this.callCommonModel(prompt);
13544
- }
13545
- // <- Note: [๐Ÿค–] callXxxModel
13546
- /**
13547
- * Calls remote proxy server to use both completion or chat model
13548
- */
13549
- async callCommonModel(prompt) {
13550
- const socket = await createRemoteClient(this.options);
13551
- socket.emit('prompt-request', {
13552
- identification: this.options.identification,
13553
- prompt,
13554
- } /* <- Note: [๐Ÿค›] */);
13555
- const promptResult = await new Promise((resolve, reject) => {
13556
- socket.on('prompt-response', (response) => {
13557
- resolve(response.promptResult);
13558
- socket.disconnect();
13559
- });
13560
- socket.on('error', (error) => {
13561
- reject(deserializeError(error));
13562
- socket.disconnect();
13563
- });
13564
- });
13565
- socket.disconnect();
13566
- return promptResult;
13567
- }
13568
- }
13569
- /**
13570
- * TODO: Maybe use `$exportJson`
13571
- * TODO: [๐Ÿง ][๐Ÿ›] Maybe not `isAnonymous: boolean` BUT `mode: 'ANONYMOUS'|'COLLECTION'`
13572
- * TODO: [๐Ÿ“] Allow to list compatible models with each variant
13573
- * TODO: [๐Ÿ—ฏ] RemoteLlmExecutionTools should extend Destroyable and implement IDestroyable
13574
- * TODO: [๐Ÿง ][๐ŸŒฐ] Allow to pass `title` for tracking purposes
13575
- * TODO: [๐Ÿง ] Maybe remove `@promptbook/remote-client` and just use `@promptbook/core`
13576
- */
13577
-
13578
13803
  /**
13579
13804
  * Function computeUsage will create price per one token based on the string value found on openai page
13580
13805
  *