@promptbook/wizard 0.103.0-46 → 0.103.0-48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/esm/index.es.js +1008 -871
  2. package/esm/index.es.js.map +1 -1
  3. package/esm/typings/servers.d.ts +1 -7
  4. package/esm/typings/src/_packages/components.index.d.ts +4 -0
  5. package/esm/typings/src/_packages/core.index.d.ts +22 -14
  6. package/esm/typings/src/_packages/types.index.d.ts +14 -6
  7. package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +7 -3
  8. package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +6 -1
  9. package/esm/typings/src/book-2.0/agent-source/AgentSourceParseResult.d.ts +3 -2
  10. package/esm/typings/src/book-2.0/agent-source/computeAgentHash.d.ts +8 -0
  11. package/esm/typings/src/book-2.0/agent-source/computeAgentHash.test.d.ts +1 -0
  12. package/esm/typings/src/book-2.0/agent-source/createCommitmentRegex.d.ts +1 -1
  13. package/esm/typings/src/book-2.0/agent-source/createDefaultAgentName.d.ts +8 -0
  14. package/esm/typings/src/book-2.0/agent-source/normalizeAgentName.d.ts +9 -0
  15. package/esm/typings/src/book-2.0/agent-source/normalizeAgentName.test.d.ts +1 -0
  16. package/esm/typings/src/book-2.0/agent-source/parseAgentSourceWithCommitments.d.ts +1 -1
  17. package/esm/typings/src/book-components/Chat/AgentChat/AgentChat.d.ts +14 -0
  18. package/esm/typings/src/book-components/Chat/AgentChat/AgentChat.test.d.ts +1 -0
  19. package/esm/typings/src/book-components/Chat/AgentChat/AgentChatProps.d.ts +13 -0
  20. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +1 -60
  21. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentsDatabaseSchema.d.ts +57 -32
  22. package/esm/typings/src/{book-2.0/commitments → commitments}/ACTION/ACTION.d.ts +1 -1
  23. package/esm/typings/src/{book-2.0/commitments → commitments}/DELETE/DELETE.d.ts +1 -1
  24. package/esm/typings/src/{book-2.0/commitments → commitments}/FORMAT/FORMAT.d.ts +1 -1
  25. package/esm/typings/src/{book-2.0/commitments → commitments}/GOAL/GOAL.d.ts +1 -1
  26. package/esm/typings/src/{book-2.0/commitments → commitments}/KNOWLEDGE/KNOWLEDGE.d.ts +1 -5
  27. package/esm/typings/src/{book-2.0/commitments → commitments}/MEMORY/MEMORY.d.ts +1 -1
  28. package/esm/typings/src/{book-2.0/commitments → commitments}/MESSAGE/MESSAGE.d.ts +1 -1
  29. package/esm/typings/src/{book-2.0/commitments → commitments}/META/META.d.ts +1 -1
  30. package/esm/typings/src/{book-2.0/commitments → commitments}/META_IMAGE/META_IMAGE.d.ts +1 -1
  31. package/esm/typings/src/{book-2.0/commitments → commitments}/META_LINK/META_LINK.d.ts +1 -1
  32. package/esm/typings/src/{book-2.0/commitments → commitments}/MODEL/MODEL.d.ts +1 -1
  33. package/esm/typings/src/{book-2.0/commitments → commitments}/NOTE/NOTE.d.ts +1 -1
  34. package/esm/typings/src/{book-2.0/commitments → commitments}/PERSONA/PERSONA.d.ts +1 -1
  35. package/esm/typings/src/{book-2.0/commitments → commitments}/RULE/RULE.d.ts +1 -1
  36. package/esm/typings/src/{book-2.0/commitments → commitments}/SAMPLE/SAMPLE.d.ts +1 -1
  37. package/esm/typings/src/{book-2.0/commitments → commitments}/SCENARIO/SCENARIO.d.ts +1 -1
  38. package/esm/typings/src/{book-2.0/commitments → commitments}/STYLE/STYLE.d.ts +1 -1
  39. package/esm/typings/src/{book-2.0/commitments → commitments}/_base/BaseCommitmentDefinition.d.ts +1 -1
  40. package/esm/typings/src/{book-2.0/commitments → commitments}/_base/CommitmentDefinition.d.ts +1 -1
  41. package/esm/typings/src/{book-2.0/commitments → commitments}/_base/NotYetImplementedCommitmentDefinition.d.ts +1 -1
  42. package/esm/typings/src/{book-2.0/commitments → commitments}/_base/createEmptyAgentModelRequirements.d.ts +1 -1
  43. package/esm/typings/src/execution/LlmExecutionTools.d.ts +1 -1
  44. package/esm/typings/src/llm-providers/_common/utils/assertUniqueModels.d.ts +12 -0
  45. package/esm/typings/src/llm-providers/agent/Agent.d.ts +10 -9
  46. package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +5 -1
  47. package/esm/typings/src/llm-providers/agent/CreateAgentLlmExecutionToolsOptions.d.ts +1 -1
  48. package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +32 -0
  49. package/esm/typings/src/llm-providers/agent/RemoteAgentOptions.d.ts +11 -0
  50. package/esm/typings/src/llm-providers/openai/OpenAiAssistantExecutionTools.d.ts +29 -4
  51. package/esm/typings/src/llm-providers/openai/openai-models.test.d.ts +4 -0
  52. package/esm/typings/src/remote-server/startAgentServer.d.ts +1 -1
  53. package/esm/typings/src/remote-server/startRemoteServer.d.ts +1 -2
  54. package/esm/typings/src/storage/_common/PromptbookStorage.d.ts +1 -0
  55. package/esm/typings/src/transpilers/openai-sdk/register.d.ts +1 -1
  56. package/esm/typings/src/types/typeAliases.d.ts +12 -0
  57. package/esm/typings/src/utils/color/internal-utils/checkChannelValue.d.ts +0 -3
  58. package/esm/typings/src/utils/normalization/normalize-to-kebab-case.d.ts +2 -0
  59. package/esm/typings/src/utils/normalization/normalizeTo_PascalCase.d.ts +3 -0
  60. package/esm/typings/src/utils/normalization/normalizeTo_camelCase.d.ts +2 -0
  61. package/esm/typings/src/utils/normalization/titleToName.d.ts +2 -0
  62. package/esm/typings/src/utils/random/$generateBookBoilerplate.d.ts +2 -2
  63. package/esm/typings/src/utils/random/$randomFullnameWithColor.d.ts +1 -1
  64. package/esm/typings/src/version.d.ts +1 -1
  65. package/package.json +2 -2
  66. package/umd/index.umd.js +1012 -875
  67. package/umd/index.umd.js.map +1 -1
  68. /package/esm/typings/src/{book-2.0/commitments → commitments}/_base/BookCommitment.d.ts +0 -0
  69. /package/esm/typings/src/{book-2.0/commitments → commitments}/_base/ParsedCommitment.d.ts +0 -0
  70. /package/esm/typings/src/{book-2.0/commitments → commitments}/index.d.ts +0 -0
package/umd/index.umd.js CHANGED
@@ -1,8 +1,8 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('spacetrim'), require('crypto'), require('socket.io-client'), require('@anthropic-ai/sdk'), require('bottleneck'), require('colors'), require('@azure/openai'), require('openai'), require('waitasecond'), require('fs/promises'), require('child_process'), require('crypto-js'), require('crypto-js/enc-hex'), require('path'), require('rxjs'), require('crypto-js/sha256'), require('mime-types'), require('papaparse'), require('@mozilla/readability'), require('jsdom'), require('showdown'), require('dotenv'), require('jszip')) :
3
- typeof define === 'function' && define.amd ? define(['exports', 'spacetrim', 'crypto', 'socket.io-client', '@anthropic-ai/sdk', 'bottleneck', 'colors', '@azure/openai', 'openai', 'waitasecond', 'fs/promises', 'child_process', 'crypto-js', 'crypto-js/enc-hex', 'path', 'rxjs', 'crypto-js/sha256', 'mime-types', 'papaparse', '@mozilla/readability', 'jsdom', 'showdown', 'dotenv', 'jszip'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["promptbook-wizard"] = {}, global.spaceTrim, global.crypto, global.socket_ioClient, global.Anthropic, global.Bottleneck, global.colors, global.openai, global.OpenAI, global.waitasecond, global.promises, global.child_process, global.cryptoJs, global.hexEncoder, global.path, global.rxjs, global.sha256, global.mimeTypes, global.papaparse, global.readability, global.jsdom, global.showdown, global.dotenv, global.JSZip));
5
- })(this, (function (exports, spaceTrim, crypto, socket_ioClient, Anthropic, Bottleneck, colors, openai, OpenAI, waitasecond, promises, child_process, cryptoJs, hexEncoder, path, rxjs, sha256, mimeTypes, papaparse, readability, jsdom, showdown, dotenv, JSZip) { 'use strict';
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('spacetrim'), require('crypto'), require('socket.io-client'), require('@anthropic-ai/sdk'), require('bottleneck'), require('colors'), require('@azure/openai'), require('openai'), require('fs/promises'), require('child_process'), require('waitasecond'), require('crypto-js'), require('crypto-js/enc-hex'), require('path'), require('rxjs'), require('crypto-js/sha256'), require('mime-types'), require('papaparse'), require('@mozilla/readability'), require('jsdom'), require('showdown'), require('dotenv'), require('jszip')) :
3
+ typeof define === 'function' && define.amd ? define(['exports', 'spacetrim', 'crypto', 'socket.io-client', '@anthropic-ai/sdk', 'bottleneck', 'colors', '@azure/openai', 'openai', 'fs/promises', 'child_process', 'waitasecond', 'crypto-js', 'crypto-js/enc-hex', 'path', 'rxjs', 'crypto-js/sha256', 'mime-types', 'papaparse', '@mozilla/readability', 'jsdom', 'showdown', 'dotenv', 'jszip'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["promptbook-wizard"] = {}, global.spaceTrim, global.crypto, global.socket_ioClient, global.Anthropic, global.Bottleneck, global.colors, global.openai, global.OpenAI, global.promises, global.child_process, global.waitasecond, global.cryptoJs, global.hexEncoder, global.path, global.rxjs, global.sha256, global.mimeTypes, global.papaparse, global.readability, global.jsdom, global.showdown, global.dotenv, global.JSZip));
5
+ })(this, (function (exports, spaceTrim, crypto, socket_ioClient, Anthropic, Bottleneck, colors, openai, OpenAI, promises, child_process, waitasecond, cryptoJs, hexEncoder, path, rxjs, sha256, mimeTypes, papaparse, readability, jsdom, showdown, dotenv, JSZip) { 'use strict';
6
6
 
7
7
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
8
8
 
@@ -48,7 +48,7 @@
48
48
  * @generated
49
49
  * @see https://github.com/webgptorg/promptbook
50
50
  */
51
- const PROMPTBOOK_ENGINE_VERSION = '0.103.0-46';
51
+ const PROMPTBOOK_ENGINE_VERSION = '0.103.0-48';
52
52
  /**
53
53
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
54
54
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -61,15 +61,20 @@
61
61
  */
62
62
  const REMOTE_SERVER_URLS = [
63
63
  {
64
- title: 'Promptbook',
65
- description: `Servers of Promptbook.studio`,
64
+ title: 'Promptbook.Studio',
65
+ description: `Server of Promptbook.studio`,
66
66
  owner: 'AI Web, LLC <legal@ptbk.io> (https://www.ptbk.io/)',
67
- isAnonymousModeAllowed: true,
68
67
  urls: [
69
68
  'https://promptbook.s5.ptbk.io/',
70
69
  // Note: Servers 1-4 are not running
71
70
  ],
72
71
  },
72
+ {
73
+ title: 'Testing Agents',
74
+ description: `Testing Agents server on Vercel`,
75
+ owner: 'AI Web, LLC <legal@ptbk.io> (https://www.ptbk.io/)',
76
+ urls: ['https://s6.ptbk.io/'],
77
+ },
73
78
  /*
74
79
  Note: Working on older version of Promptbook and not supported anymore
75
80
  {
@@ -327,9 +332,6 @@
327
332
  throw new Error(`${channelName} channel is greater than 255, it is ${value}`);
328
333
  }
329
334
  }
330
- /**
331
- * TODO: [🧠][🚓] Is/which combination it better to use asserts/check, validate or is utility function?
332
- */
333
335
 
334
336
  /**
335
337
  * Color object represents an RGB color with alpha channel
@@ -3869,17 +3871,17 @@
3869
3871
  },
3870
3872
  /**/
3871
3873
  /*/
3872
- {
3873
- modelTitle: 'tts-1-hd-1106',
3874
- modelName: 'tts-1-hd-1106',
3875
- },
3876
- /**/
3874
+ {
3875
+ modelTitle: 'tts-1-hd-1106',
3876
+ modelName: 'tts-1-hd-1106',
3877
+ },
3878
+ /**/
3877
3879
  /*/
3878
- {
3879
- modelTitle: 'tts-1-hd',
3880
- modelName: 'tts-1-hd',
3881
- },
3882
- /**/
3880
+ {
3881
+ modelTitle: 'tts-1-hd',
3882
+ modelName: 'tts-1-hd',
3883
+ },
3884
+ /**/
3883
3885
  /**/
3884
3886
  {
3885
3887
  modelVariant: 'CHAT',
@@ -5217,26 +5219,6 @@
5217
5219
  output: pricing(`$0.45 / 1M tokens`),
5218
5220
  },
5219
5221
  },
5220
- {
5221
- modelVariant: 'CHAT',
5222
- modelTitle: 'Gemini 2.0 Flash',
5223
- modelName: 'gemini-2.0-flash',
5224
- modelDescription: 'Fast, efficient model with 128K context window optimized for rapid response times. Balances performance and cost with 2x lower latency than Pro models while maintaining strong capabilities in text generation, code completion, and logical reasoning. Excellent for interactive applications, chatbots, and services requiring quick responses with good quality.',
5225
- pricing: {
5226
- prompt: pricing(`$0.35 / 1M tokens`),
5227
- output: pricing(`$1.05 / 1M tokens`),
5228
- },
5229
- },
5230
- {
5231
- modelVariant: 'CHAT',
5232
- modelTitle: 'Gemini 2.0 Flash Lite',
5233
- modelName: 'gemini-2.0-flash-lite',
5234
- modelDescription: 'Streamlined version of Gemini 2.0 Flash with 64K context window, designed for extremely low-latency applications. Features 40% smaller model size and 3x faster inference while retaining core capabilities for text and simple reasoning tasks. Perfect for mobile applications, edge deployments, and high-volume services with strict latency requirements.',
5235
- pricing: {
5236
- prompt: pricing(`$0.20 / 1M tokens`),
5237
- output: pricing(`$0.60 / 1M tokens`),
5238
- },
5239
- },
5240
5222
  {
5241
5223
  modelVariant: 'CHAT',
5242
5224
  modelTitle: 'Gemini 2.0 Flash Thinking',
@@ -6813,7 +6795,7 @@
6813
6795
  *
6814
6796
  * This is useful for calling OpenAI API with a single assistant, for more wide usage use `OpenAiExecutionTools`.
6815
6797
  *
6816
- * Note: [🦖] There are several different things in Promptbook:
6798
+ * !!! Note: [🦖] There are several different things in Promptbook:
6817
6799
  * - `Agent` - which represents an AI Agent with its source, memories, actions, etc. Agent is a higher-level abstraction which is internally using:
6818
6800
  * - `LlmExecutionTools` - which wraps one or more LLM models and provides an interface to execute them
6819
6801
  * - `AgentLlmExecutionTools` - which is a specific implementation of `LlmExecutionTools` that wraps another LlmExecutionTools and applies agent-specific system prompts and requirements
@@ -6919,17 +6901,21 @@
6919
6901
  console.info('connect', stream.currentEvent);
6920
6902
  }
6921
6903
  });
6904
+ /*
6922
6905
  stream.on('messageDelta', (messageDelta) => {
6923
- var _a;
6924
- if (this.options.isVerbose &&
6906
+ if (
6907
+ this.options.isVerbose &&
6925
6908
  messageDelta &&
6926
6909
  messageDelta.content &&
6927
6910
  messageDelta.content[0] &&
6928
- messageDelta.content[0].type === 'text') {
6929
- console.info('messageDelta', (_a = messageDelta.content[0].text) === null || _a === void 0 ? void 0 : _a.value);
6911
+ messageDelta.content[0].type === 'text'
6912
+ ) {
6913
+ console.info('messageDelta', messageDelta.content[0].text?.value);
6930
6914
  }
6915
+
6931
6916
  // <- TODO: [🐚] Make streaming and running tasks working
6932
6917
  });
6918
+ */
6933
6919
  stream.on('messageCreated', (message) => {
6934
6920
  if (this.options.isVerbose) {
6935
6921
  console.info('messageCreated', message);
@@ -6984,15 +6970,19 @@
6984
6970
  },
6985
6971
  });
6986
6972
  }
6987
- async playground() {
6973
+ /*
6974
+ public async playground() {
6988
6975
  const client = await this.getClient();
6976
+
6989
6977
  // List all assistants
6990
6978
  const assistants = await client.beta.assistants.list();
6991
6979
  console.log('!!! Assistants:', assistants);
6980
+
6992
6981
  // Get details of a specific assistant
6993
6982
  const assistantId = 'asst_MO8fhZf4dGloCfXSHeLcIik0';
6994
6983
  const assistant = await client.beta.assistants.retrieve(assistantId);
6995
6984
  console.log('!!! Assistant Details:', assistant);
6985
+
6996
6986
  // Update an assistant
6997
6987
  const updatedAssistant = await client.beta.assistants.update(assistantId, {
6998
6988
  name: assistant.name + '(M)',
@@ -7002,71 +6992,103 @@
7002
6992
  },
7003
6993
  });
7004
6994
  console.log('!!! Updated Assistant:', updatedAssistant);
7005
- await waitasecond.forEver();
6995
+
6996
+ await forEver();
6997
+ }
6998
+ */
6999
+ /**
7000
+ * Get an existing assistant tool wrapper
7001
+ */
7002
+ getAssistant(assistantId) {
7003
+ return new OpenAiAssistantExecutionTools({
7004
+ ...this.options,
7005
+ assistantId,
7006
+ });
7006
7007
  }
7007
7008
  async createNewAssistant(options) {
7008
7009
  if (!this.isCreatingNewAssistantsAllowed) {
7009
7010
  throw new NotAllowed(`Creating new assistants is not allowed. Set \`isCreatingNewAssistantsAllowed: true\` in options to enable this feature.`);
7010
7011
  }
7011
7012
  // await this.playground();
7012
- const { name, instructions } = options;
7013
+ const { name, instructions, knowledgeSources } = options;
7013
7014
  const client = await this.getClient();
7014
- /*/
7015
- //TODO: !!!
7016
- async function downloadFile(url: string, folder = './tmp'): Promise<string> {
7017
- const filename = path.basename(url.split('?')[0]);
7018
- const filepath = path.join(folder, filename);
7019
-
7020
- if (!fs.existsSync(folder)) fs.mkdirSync(folder);
7021
-
7022
- const res = await fetch(url);
7023
- if (!res.ok) throw new Error(`Download error: ${url}`);
7024
- const buffer = await res.arrayBuffer();
7025
- fs.writeFileSync(filepath, Buffer.from(buffer));
7026
- console.log(`📥 File downloaded: ${filename}`);
7027
-
7028
- return filepath;
7029
- }
7030
-
7031
- async function uploadFileToOpenAI(filepath: string) {
7032
- const file = await client.files.create({
7033
- file: fs.createReadStream(filepath),
7034
- purpose: 'assistants',
7015
+ let vectorStoreId;
7016
+ // If knowledge sources are provided, create a vector store with them
7017
+ if (knowledgeSources && knowledgeSources.length > 0) {
7018
+ if (this.options.isVerbose) {
7019
+ console.info(`📚 Creating vector store with ${knowledgeSources.length} knowledge sources...`);
7020
+ }
7021
+ // Create a vector store
7022
+ const vectorStore = await client.beta.vectorStores.create({
7023
+ name: `${name} Knowledge Base`,
7035
7024
  });
7036
- console.log(`⬆️ File uploaded to OpenAI: ${file.filename} (${file.id})`);
7037
- return file;
7038
- }
7039
-
7040
- // 🌐 URL addresses of files to upload
7041
- const fileUrls = [
7042
- 'https://raw.githubusercontent.com/vercel/next.js/canary/packages/next/README.md',
7043
- 'https://raw.githubusercontent.com/openai/openai-cookbook/main/examples/How_to_call_the_Assistants_API_with_Node.js.ipynb',
7044
- ];
7045
-
7046
- // 1️⃣ Download files from URL
7047
- const localFiles = [];
7048
- for (const url of fileUrls) {
7049
- const filepath = await downloadFile(url);
7050
- localFiles.push(filepath);
7051
- }
7052
-
7053
- // 2️⃣ Upload files to OpenAI
7054
- const uploadedFiles = [];
7055
- for (const filepath of localFiles) {
7056
- const file = await uploadFileToOpenAI(filepath);
7057
- uploadedFiles.push(file.id);
7025
+ vectorStoreId = vectorStore.id;
7026
+ if (this.options.isVerbose) {
7027
+ console.info(`✅ Vector store created: ${vectorStoreId}`);
7028
+ }
7029
+ // Upload files from knowledge sources to the vector store
7030
+ const fileStreams = [];
7031
+ for (const source of knowledgeSources) {
7032
+ try {
7033
+ // Check if it's a URL
7034
+ if (source.startsWith('http://') || source.startsWith('https://')) {
7035
+ // Download the file
7036
+ const response = await fetch(source);
7037
+ if (!response.ok) {
7038
+ console.error(`Failed to download ${source}: ${response.statusText}`);
7039
+ continue;
7040
+ }
7041
+ const buffer = await response.arrayBuffer();
7042
+ const filename = source.split('/').pop() || 'downloaded-file';
7043
+ const blob = new Blob([buffer]);
7044
+ const file = new File([blob], filename);
7045
+ fileStreams.push(file);
7046
+ }
7047
+ else {
7048
+ // Assume it's a local file path
7049
+ // Note: This will work in Node.js environment
7050
+ // For browser environments, this would need different handling
7051
+ const fs = await import('fs');
7052
+ const fileStream = fs.createReadStream(source);
7053
+ fileStreams.push(fileStream);
7054
+ }
7055
+ }
7056
+ catch (error) {
7057
+ console.error(`Error processing knowledge source ${source}:`, error);
7058
+ }
7059
+ }
7060
+ // Batch upload files to the vector store
7061
+ if (fileStreams.length > 0) {
7062
+ try {
7063
+ await client.beta.vectorStores.fileBatches.uploadAndPoll(vectorStoreId, {
7064
+ files: fileStreams,
7065
+ });
7066
+ if (this.options.isVerbose) {
7067
+ console.info(`✅ Uploaded ${fileStreams.length} files to vector store`);
7068
+ }
7069
+ }
7070
+ catch (error) {
7071
+ console.error('Error uploading files to vector store:', error);
7072
+ }
7073
+ }
7058
7074
  }
7059
- /**/
7060
- // alert('!!!! Creating new OpenAI assistant');
7061
- // 3️⃣ Create assistant with uploaded files
7062
- const assistant = await client.beta.assistants.create({
7075
+ // Create assistant with vector store attached
7076
+ const assistantConfig = {
7063
7077
  name,
7064
7078
  description: 'Assistant created via Promptbook',
7065
7079
  model: 'gpt-4o',
7066
7080
  instructions,
7067
7081
  tools: [/* TODO: [🧠] Maybe add { type: 'code_interpreter' }, */ { type: 'file_search' }],
7068
- // !!!! file_ids: uploadedFiles,
7069
- });
7082
+ };
7083
+ // Attach vector store if created
7084
+ if (vectorStoreId) {
7085
+ assistantConfig.tool_resources = {
7086
+ file_search: {
7087
+ vector_store_ids: [vectorStoreId],
7088
+ },
7089
+ };
7090
+ }
7091
+ const assistant = await client.beta.assistants.create(assistantConfig);
7070
7092
  console.log(`✅ Assistant created: ${assistant.id}`);
7071
7093
  // TODO: !!!! Try listing existing assistants
7072
7094
  // TODO: !!!! Try marking existing assistants by DISCRIMINANT
@@ -7077,6 +7099,95 @@
7077
7099
  assistantId: assistant.id,
7078
7100
  });
7079
7101
  }
7102
+ async updateAssistant(options) {
7103
+ if (!this.isCreatingNewAssistantsAllowed) {
7104
+ throw new NotAllowed(`Updating assistants is not allowed. Set \`isCreatingNewAssistantsAllowed: true\` in options to enable this feature.`);
7105
+ }
7106
+ const { assistantId, name, instructions, knowledgeSources } = options;
7107
+ const client = await this.getClient();
7108
+ let vectorStoreId;
7109
+ // If knowledge sources are provided, create a vector store with them
7110
+ // TODO: [🧠] Reuse vector store creation logic from createNewAssistant
7111
+ if (knowledgeSources && knowledgeSources.length > 0) {
7112
+ if (this.options.isVerbose) {
7113
+ console.info(`📚 Creating vector store for update with ${knowledgeSources.length} knowledge sources...`);
7114
+ }
7115
+ // Create a vector store
7116
+ const vectorStore = await client.beta.vectorStores.create({
7117
+ name: `${name} Knowledge Base`,
7118
+ });
7119
+ vectorStoreId = vectorStore.id;
7120
+ if (this.options.isVerbose) {
7121
+ console.info(`✅ Vector store created: ${vectorStoreId}`);
7122
+ }
7123
+ // Upload files from knowledge sources to the vector store
7124
+ const fileStreams = [];
7125
+ for (const source of knowledgeSources) {
7126
+ try {
7127
+ // Check if it's a URL
7128
+ if (source.startsWith('http://') || source.startsWith('https://')) {
7129
+ // Download the file
7130
+ const response = await fetch(source);
7131
+ if (!response.ok) {
7132
+ console.error(`Failed to download ${source}: ${response.statusText}`);
7133
+ continue;
7134
+ }
7135
+ const buffer = await response.arrayBuffer();
7136
+ const filename = source.split('/').pop() || 'downloaded-file';
7137
+ const blob = new Blob([buffer]);
7138
+ const file = new File([blob], filename);
7139
+ fileStreams.push(file);
7140
+ }
7141
+ else {
7142
+ // Assume it's a local file path
7143
+ // Note: This will work in Node.js environment
7144
+ // For browser environments, this would need different handling
7145
+ const fs = await import('fs');
7146
+ const fileStream = fs.createReadStream(source);
7147
+ fileStreams.push(fileStream);
7148
+ }
7149
+ }
7150
+ catch (error) {
7151
+ console.error(`Error processing knowledge source ${source}:`, error);
7152
+ }
7153
+ }
7154
+ // Batch upload files to the vector store
7155
+ if (fileStreams.length > 0) {
7156
+ try {
7157
+ await client.beta.vectorStores.fileBatches.uploadAndPoll(vectorStoreId, {
7158
+ files: fileStreams,
7159
+ });
7160
+ if (this.options.isVerbose) {
7161
+ console.info(`✅ Uploaded ${fileStreams.length} files to vector store`);
7162
+ }
7163
+ }
7164
+ catch (error) {
7165
+ console.error('Error uploading files to vector store:', error);
7166
+ }
7167
+ }
7168
+ }
7169
+ const assistantUpdate = {
7170
+ name,
7171
+ instructions,
7172
+ tools: [/* TODO: [🧠] Maybe add { type: 'code_interpreter' }, */ { type: 'file_search' }],
7173
+ };
7174
+ if (vectorStoreId) {
7175
+ assistantUpdate.tool_resources = {
7176
+ file_search: {
7177
+ vector_store_ids: [vectorStoreId],
7178
+ },
7179
+ };
7180
+ }
7181
+ const assistant = await client.beta.assistants.update(assistantId, assistantUpdate);
7182
+ if (this.options.isVerbose) {
7183
+ console.log(`✅ Assistant updated: ${assistant.id}`);
7184
+ }
7185
+ return new OpenAiAssistantExecutionTools({
7186
+ ...this.options,
7187
+ isCreatingNewAssistantsAllowed: false,
7188
+ assistantId: assistant.id,
7189
+ });
7190
+ }
7080
7191
  /**
7081
7192
  * Discriminant for type guards
7082
7193
  */
@@ -7534,6 +7645,8 @@
7534
7645
  /**
7535
7646
  * Converts a given text to kebab-case format.
7536
7647
  *
7648
+ * Note: [🔂] This function is idempotent.
7649
+ *
7537
7650
  * @param text The text to be converted.
7538
7651
  * @returns The kebab-case formatted string.
7539
7652
  * @example 'hello-world'
@@ -7659,6 +7772,8 @@
7659
7772
  /**
7660
7773
  * Converts a title string into a normalized name.
7661
7774
  *
7775
+ * Note: [🔂] This function is idempotent.
7776
+ *
7662
7777
  * @param value The title string to be converted to a name.
7663
7778
  * @returns A normalized name derived from the input title.
7664
7779
  * @example 'Hello World!' -> 'hello-world'
@@ -13278,40 +13393,6 @@
13278
13393
  * Note: [💞] Ignore a discrepancy between file name and entity name
13279
13394
  */
13280
13395
 
13281
- /**
13282
- * Creates an empty/basic agent model requirements object
13283
- * This serves as the starting point for the reduce-like pattern
13284
- * where each commitment applies its changes to build the final requirements
13285
- *
13286
- * @public exported from `@promptbook/core`
13287
- */
13288
- function createEmptyAgentModelRequirements() {
13289
- return {
13290
- systemMessage: '',
13291
- // modelName: 'gpt-5',
13292
- modelName: 'gemini-2.5-flash-lite',
13293
- temperature: 0.7,
13294
- topP: 0.9,
13295
- topK: 50,
13296
- };
13297
- }
13298
- /**
13299
- * Creates a basic agent model requirements with just the agent name
13300
- * This is used when we have an agent name but no commitments
13301
- *
13302
- * @public exported from `@promptbook/core`
13303
- */
13304
- function createBasicAgentModelRequirements(agentName) {
13305
- const empty = createEmptyAgentModelRequirements();
13306
- return {
13307
- ...empty,
13308
- systemMessage: `You are ${agentName || 'AI Agent'}`,
13309
- };
13310
- }
13311
- /**
13312
- * TODO: [🐤] Deduplicate `AgentModelRequirements` and `ModelRequirements` model requirements
13313
- */
13314
-
13315
13396
  /**
13316
13397
  * Generates a regex pattern to match a specific commitment
13317
13398
  *
@@ -13845,23 +13926,19 @@
13845
13926
  `);
13846
13927
  }
13847
13928
  applyToAgentModelRequirements(requirements, content) {
13848
- var _a;
13849
13929
  const trimmedContent = content.trim();
13850
13930
  if (!trimmedContent) {
13851
13931
  return requirements;
13852
13932
  }
13853
13933
  // Check if content is a URL (external knowledge source)
13854
- if (this.isUrl(trimmedContent)) {
13934
+ if (isValidUrl(trimmedContent)) {
13855
13935
  // Store the URL for later async processing
13856
13936
  const updatedRequirements = {
13857
13937
  ...requirements,
13858
- metadata: {
13859
- ...requirements.metadata,
13860
- knowledgeSources: [
13861
- ...(((_a = requirements.metadata) === null || _a === void 0 ? void 0 : _a.knowledgeSources) || []),
13862
- trimmedContent,
13863
- ],
13864
- },
13938
+ knowledgeSources: [
13939
+ ...(requirements.knowledgeSources || []),
13940
+ trimmedContent,
13941
+ ],
13865
13942
  };
13866
13943
  // Add placeholder information about knowledge sources to system message
13867
13944
  const knowledgeInfo = `Knowledge Source URL: ${trimmedContent} (will be processed for retrieval during chat)`;
@@ -13873,18 +13950,6 @@
13873
13950
  return this.appendToSystemMessage(requirements, knowledgeSection, '\n\n');
13874
13951
  }
13875
13952
  }
13876
- /**
13877
- * Check if content is a URL
13878
- */
13879
- isUrl(content) {
13880
- try {
13881
- new URL(content);
13882
- return true;
13883
- }
13884
- catch (_a) {
13885
- return false;
13886
- }
13887
- }
13888
13953
  }
13889
13954
  /**
13890
13955
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -14695,6 +14760,7 @@
14695
14760
  // Keep everything after the PERSONA section
14696
14761
  cleanedMessage = lines.slice(personaEndIndex).join('\n').trim();
14697
14762
  }
14763
+ // TODO: [🕛] There should be `agentFullname` not `agentName`
14698
14764
  // Create new system message with persona at the beginning
14699
14765
  // Format: "You are {agentName}\n{personaContent}"
14700
14766
  // The # PERSONA comment will be removed later by removeCommentsFromSystemMessage
@@ -15182,6 +15248,40 @@
15182
15248
  * Note: [💞] Ignore a discrepancy between file name and entity name
15183
15249
  */
15184
15250
 
15251
+ /**
15252
+ * Creates an empty/basic agent model requirements object
15253
+ * This serves as the starting point for the reduce-like pattern
15254
+ * where each commitment applies its changes to build the final requirements
15255
+ *
15256
+ * @public exported from `@promptbook/core`
15257
+ */
15258
+ function createEmptyAgentModelRequirements() {
15259
+ return {
15260
+ systemMessage: '',
15261
+ // modelName: 'gpt-5',
15262
+ modelName: 'gemini-2.5-flash-lite',
15263
+ temperature: 0.7,
15264
+ topP: 0.9,
15265
+ topK: 50,
15266
+ };
15267
+ }
15268
+ /**
15269
+ * Creates a basic agent model requirements with just the agent name
15270
+ * This is used when we have an agent name but no commitments
15271
+ *
15272
+ * @public exported from `@promptbook/core`
15273
+ */
15274
+ function createBasicAgentModelRequirements(agentName) {
15275
+ const empty = createEmptyAgentModelRequirements();
15276
+ return {
15277
+ ...empty,
15278
+ systemMessage: `You are ${agentName || 'AI Agent'}`,
15279
+ };
15280
+ }
15281
+ /**
15282
+ * TODO: [🐤] Deduplicate `AgentModelRequirements` and `ModelRequirements` model requirements
15283
+ */
15284
+
15185
15285
  /**
15186
15286
  * Parses agent source using the new commitment system with multiline support
15187
15287
  * This function replaces the hardcoded commitment parsing in the original parseAgentSource
@@ -15272,29 +15372,6 @@
15272
15372
  };
15273
15373
  }
15274
15374
 
15275
- /**
15276
- * Removes comment lines (lines starting with #) from a system message
15277
- * This is used to clean up the final system message before sending it to the AI model
15278
- * while preserving the original content with comments in metadata
15279
- *
15280
- * @param systemMessage The system message that may contain comment lines
15281
- * @returns The system message with comment lines removed
15282
- *
15283
- * @private - TODO: [🧠] Maybe should be public?
15284
- */
15285
- function removeCommentsFromSystemMessage(systemMessage) {
15286
- if (!systemMessage) {
15287
- return systemMessage;
15288
- }
15289
- const lines = systemMessage.split('\n');
15290
- const filteredLines = lines.filter((line) => {
15291
- const trimmedLine = line.trim();
15292
- // Remove lines that start with # (comments)
15293
- return !trimmedLine.startsWith('#');
15294
- });
15295
- return filteredLines.join('\n').trim();
15296
- }
15297
-
15298
15375
  /**
15299
15376
  * Parses parameters from text using both supported notations:
15300
15377
  * 1. @Parameter - single word parameter starting with @
@@ -15354,17 +15431,40 @@
15354
15431
  }
15355
15432
 
15356
15433
  /**
15357
- * Creates agent model requirements using the new commitment system
15358
- * This function uses a reduce-like pattern where each commitment applies its changes
15359
- * to build the final requirements starting from a basic empty model
15434
+ * Removes comment lines (lines starting with #) from a system message
15435
+ * This is used to clean up the final system message before sending it to the AI model
15436
+ * while preserving the original content with comments in metadata
15360
15437
  *
15361
- * @public exported from `@promptbook/core`
15438
+ * @param systemMessage The system message that may contain comment lines
15439
+ * @returns The system message with comment lines removed
15440
+ *
15441
+ * @private - TODO: [🧠] Maybe should be public?
15362
15442
  */
15363
- async function createAgentModelRequirementsWithCommitments(agentSource, modelName) {
15364
- // Parse the agent source to extract commitments
15365
- const parseResult = parseAgentSourceWithCommitments(agentSource);
15366
- // Apply DELETE filtering: remove prior commitments tagged by parameters targeted by DELETE/CANCEL/DISCARD/REMOVE
15367
- const filteredCommitments = [];
15443
+ function removeCommentsFromSystemMessage(systemMessage) {
15444
+ if (!systemMessage) {
15445
+ return systemMessage;
15446
+ }
15447
+ const lines = systemMessage.split('\n');
15448
+ const filteredLines = lines.filter((line) => {
15449
+ const trimmedLine = line.trim();
15450
+ // Remove lines that start with # (comments)
15451
+ return !trimmedLine.startsWith('#');
15452
+ });
15453
+ return filteredLines.join('\n').trim();
15454
+ }
15455
+
15456
+ /**
15457
+ * Creates agent model requirements using the new commitment system
15458
+ * This function uses a reduce-like pattern where each commitment applies its changes
15459
+ * to build the final requirements starting from a basic empty model
15460
+ *
15461
+ * @public exported from `@promptbook/core`
15462
+ */
15463
+ async function createAgentModelRequirementsWithCommitments(agentSource, modelName) {
15464
+ // Parse the agent source to extract commitments
15465
+ const parseResult = parseAgentSourceWithCommitments(agentSource);
15466
+ // Apply DELETE filtering: remove prior commitments tagged by parameters targeted by DELETE/CANCEL/DISCARD/REMOVE
15467
+ const filteredCommitments = [];
15368
15468
  for (const commitment of parseResult.commitments) {
15369
15469
  // Handle DELETE-like commitments by invalidating prior tagged commitments
15370
15470
  if (commitment.type === 'DELETE' ||
@@ -15454,6 +15554,8 @@
15454
15554
  /**
15455
15555
  * Normalizes a given text to camelCase format.
15456
15556
  *
15557
+ * Note: [🔂] This function is idempotent.
15558
+ *
15457
15559
  * @param text The text to be normalized.
15458
15560
  * @param _isFirstLetterCapital Whether the first letter should be capitalized.
15459
15561
  * @returns The camelCase formatted string.
@@ -15542,154 +15644,501 @@
15542
15644
  */
15543
15645
 
15544
15646
  /**
15545
- * Parses basic information from agent source
15546
- *
15547
- * There are 2 similar functions:
15548
- * - `parseAgentSource` which is a lightweight parser for agent source, it parses basic information and its purpose is to be quick and synchronous. The commitments there are hardcoded.
15549
- * - `createAgentModelRequirements` which is an asynchronous function that creates model requirements it applies each commitment one by one and works asynchronously.
15647
+ * Computes SHA-256 hash of the agent source
15550
15648
  *
15551
15649
  * @public exported from `@promptbook/core`
15552
15650
  */
15553
- function parseAgentSource(agentSource) {
15554
- const parseResult = parseAgentSourceWithCommitments(agentSource);
15555
- // Find PERSONA and META commitments
15556
- let personaDescription = null;
15557
- for (const commitment of parseResult.commitments) {
15558
- if (commitment.type !== 'PERSONA') {
15559
- continue;
15560
- }
15561
- if (personaDescription === null) {
15562
- personaDescription = '';
15563
- }
15564
- else {
15565
- personaDescription += `\n\n${personaDescription}`;
15651
+ function computeAgentHash(agentSource) {
15652
+ return cryptoJs.SHA256(hexEncoder__default["default"].parse(agentSource /* <- TODO: !!!!! spaceTrim */)).toString( /* hex */);
15653
+ }
15654
+
15655
+ /**
15656
+ * Function parseNumber will parse number from string
15657
+ *
15658
+ * Note: [🔂] This function is idempotent.
15659
+ * Unlike Number.parseInt, Number.parseFloat it will never ever result in NaN
15660
+ * Note: it also works only with decimal numbers
15661
+ *
15662
+ * @returns parsed number
15663
+ * @throws {ParseError} if the value is not a number
15664
+ *
15665
+ * @public exported from `@promptbook/utils`
15666
+ */
15667
+ function parseNumber(value) {
15668
+ const originalValue = value;
15669
+ if (typeof value === 'number') {
15670
+ value = value.toString(); // <- TODO: Maybe more efficient way to do this
15671
+ }
15672
+ if (typeof value !== 'string') {
15673
+ return 0;
15674
+ }
15675
+ value = value.trim();
15676
+ if (value.startsWith('+')) {
15677
+ return parseNumber(value.substring(1));
15678
+ }
15679
+ if (value.startsWith('-')) {
15680
+ const number = parseNumber(value.substring(1));
15681
+ if (number === 0) {
15682
+ return 0; // <- Note: To prevent -0
15566
15683
  }
15567
- personaDescription += commitment.content;
15684
+ return -number;
15568
15685
  }
15569
- const meta = {};
15570
- for (const commitment of parseResult.commitments) {
15571
- if (commitment.type !== 'META') {
15572
- continue;
15686
+ value = value.replace(/,/g, '.');
15687
+ value = value.toUpperCase();
15688
+ if (value === '') {
15689
+ return 0;
15690
+ }
15691
+ if (value === '♾' || value.startsWith('INF')) {
15692
+ return Infinity;
15693
+ }
15694
+ if (value.includes('/')) {
15695
+ const [numerator_, denominator_] = value.split('/');
15696
+ const numerator = parseNumber(numerator_);
15697
+ const denominator = parseNumber(denominator_);
15698
+ if (denominator === 0) {
15699
+ throw new ParseError(`Unable to parse number from "${originalValue}" because denominator is zero`);
15573
15700
  }
15574
- // Parse META commitments - format is "META TYPE content"
15575
- const metaTypeRaw = commitment.content.split(' ')[0] || 'NONE';
15576
- const metaType = normalizeTo_camelCase(metaTypeRaw);
15577
- meta[metaType] = spaceTrim__default["default"](commitment.content.substring(metaTypeRaw.length));
15701
+ return numerator / denominator;
15578
15702
  }
15579
- // Generate gravatar fallback if no meta image specified
15580
- if (!meta.image) {
15581
- meta.image = generatePlaceholderAgentProfileImageUrl(parseResult.agentName || '!!');
15703
+ if (/^(NAN|NULL|NONE|UNDEFINED|ZERO|NO.*)$/.test(value)) {
15704
+ return 0;
15582
15705
  }
15583
- // Parse parameters using unified approach - both @Parameter and {parameter} notations
15584
- // are treated as the same syntax feature with unified representation
15585
- const parameters = parseParameters(agentSource);
15586
- return {
15587
- agentName: parseResult.agentName,
15588
- personaDescription,
15589
- meta,
15590
- parameters,
15591
- };
15706
+ if (value.includes('E')) {
15707
+ const [significand, exponent] = value.split('E');
15708
+ return parseNumber(significand) * 10 ** parseNumber(exponent);
15709
+ }
15710
+ if (!/^[0-9.]+$/.test(value) || value.split('.').length > 2) {
15711
+ throw new ParseError(`Unable to parse number from "${originalValue}"`);
15712
+ }
15713
+ const num = parseFloat(value);
15714
+ if (isNaN(num)) {
15715
+ throw new ParseError(`Unexpected NaN when parsing number from "${originalValue}"`);
15716
+ }
15717
+ return num;
15592
15718
  }
15593
15719
  /**
15594
- * TODO: [🕛] Unite `AgentBasicInformation`, `ChatParticipant`, `LlmExecutionTools` + `LlmToolsMetadata`
15720
+ * TODO: Maybe use sth. like safe-eval in fraction/calculation case @see https://www.npmjs.com/package/safe-eval
15721
+ * TODO: [🧠][🌻] Maybe export through `@promptbook/markdown-utils` not `@promptbook/utils`
15595
15722
  */
15596
15723
 
15597
15724
  /**
15598
- * Creates model requirements for an agent based on its source
15725
+ * Makes first letter of a string lowercase
15599
15726
  *
15600
- * There are 2 similar functions:
15601
- * - `parseAgentSource` which is a lightweight parser for agent source, it parses basic information and its purpose is to be quick and synchronous. The commitments there are hardcoded.
15602
- * - `createAgentModelRequirements` which is an asynchronous function that creates model requirements it applies each commitment one by one and works asynchronous.
15727
+ * Note: [🔂] This function is idempotent.
15603
15728
  *
15604
- * @public exported from `@promptbook/core`
15729
+ * @public exported from `@promptbook/utils`
15605
15730
  */
15606
- async function createAgentModelRequirements(agentSource, modelName, availableModels, llmTools) {
15607
- // If availableModels are provided and no specific modelName is given,
15608
- // use preparePersona to select the best model
15609
- if (availableModels && !modelName && llmTools) {
15610
- const selectedModelName = await selectBestModelUsingPersona(agentSource, llmTools);
15611
- return createAgentModelRequirementsWithCommitments(agentSource, selectedModelName);
15612
- }
15613
- // Use the new commitment-based system with provided or default model
15614
- return createAgentModelRequirementsWithCommitments(agentSource, modelName);
15731
+ function decapitalize(word) {
15732
+ return word.substring(0, 1).toLowerCase() + word.substring(1);
15615
15733
  }
15734
+
15616
15735
  /**
15617
- * Selects the best model using the preparePersona function
15618
- * This directly uses preparePersona to ensure DRY principle
15736
+ * Parses keywords from a string
15619
15737
  *
15620
- * @param agentSource The agent source to derive persona description from
15621
- * @param llmTools LLM tools for preparing persona
15622
- * @returns The name of the best selected model
15623
- * @private function of `createAgentModelRequirements`
15738
+ * @param {string} input
15739
+ * @returns {Set} of keywords without diacritics in lowercase
15740
+ * @public exported from `@promptbook/utils`
15624
15741
  */
15625
- async function selectBestModelUsingPersona(agentSource, llmTools) {
15626
- var _a;
15627
- // Parse agent source to get persona description
15628
- const { agentName, personaDescription } = parseAgentSource(agentSource);
15629
- // Use agent name as fallback if no persona description is available
15630
- const description = personaDescription || agentName || 'AI Agent';
15631
- try {
15632
- // Use preparePersona directly
15633
- const { modelsRequirements } = await preparePersona(description, { llm: llmTools }, { isVerbose: false });
15634
- // Extract the first model name from the requirements
15635
- if (modelsRequirements.length > 0 && ((_a = modelsRequirements[0]) === null || _a === void 0 ? void 0 : _a.modelName)) {
15636
- return modelsRequirements[0].modelName;
15637
- }
15638
- // Fallback: get available models and return the first CHAT model
15639
- const availableModels = await llmTools.listModels();
15640
- const chatModels = availableModels.filter(({ modelVariant }) => modelVariant === 'CHAT');
15641
- if (chatModels.length === 0) {
15642
- throw new Error('No CHAT models available for agent model selection');
15643
- }
15644
- return chatModels[0].modelName;
15645
- }
15646
- catch (error) {
15647
- console.warn('Failed to use preparePersona for model selection, falling back to first available model:', error);
15648
- // Fallback: get available models and return the first CHAT model
15649
- const availableModels = await llmTools.listModels();
15650
- const chatModels = availableModels.filter(({ modelVariant }) => modelVariant === 'CHAT');
15651
- if (chatModels.length === 0) {
15652
- throw new Error('No CHAT models available for agent model selection');
15653
- }
15654
- return chatModels[0].modelName;
15655
- }
15742
+ function parseKeywordsFromString(input) {
15743
+ const keywords = normalizeTo_SCREAMING_CASE(removeDiacritics(input))
15744
+ .toLowerCase()
15745
+ .split(/[^a-z0-9]+/gs)
15746
+ .filter((value) => value);
15747
+ return new Set(keywords);
15656
15748
  }
15749
+
15657
15750
  /**
15658
- * Extracts MCP servers from agent source
15751
+ * Converts a name string into a URI-compatible format.
15659
15752
  *
15660
- * @param agentSource The agent source string that may contain MCP lines
15661
- * @returns Array of MCP server identifiers
15753
+ * @param name The string to be converted to a URI-compatible format.
15754
+ * @returns A URI-compatible string derived from the input name.
15755
+ * @example 'Hello World' -> 'hello-world'
15756
+ * @public exported from `@promptbook/utils`
15757
+ */
15758
+ function nameToUriPart(name) {
15759
+ let uriPart = name;
15760
+ uriPart = uriPart.toLowerCase();
15761
+ uriPart = removeDiacritics(uriPart);
15762
+ uriPart = uriPart.replace(/[^a-zA-Z0-9]+/g, '-');
15763
+ uriPart = uriPart.replace(/^-+/, '');
15764
+ uriPart = uriPart.replace(/-+$/, '');
15765
+ return uriPart;
15766
+ }
15767
+
15768
+ /**
15769
+ * Converts a given name into URI-compatible parts.
15662
15770
  *
15663
- * @private TODO: [🧠] Maybe should be public
15771
+ * @param name The name to be converted into URI parts.
15772
+ * @returns An array of URI-compatible parts derived from the name.
15773
+ * @example 'Example Name' -> ['example', 'name']
15774
+ * @public exported from `@promptbook/utils`
15664
15775
  */
15665
- function extractMcpServers(agentSource) {
15666
- if (!agentSource) {
15667
- return [];
15668
- }
15669
- const lines = agentSource.split('\n');
15670
- const mcpRegex = /^\s*MCP\s+(.+)$/i;
15671
- const mcpServers = [];
15672
- // Look for MCP lines
15673
- for (const line of lines) {
15674
- const match = line.match(mcpRegex);
15675
- if (match && match[1]) {
15676
- mcpServers.push(match[1].trim());
15677
- }
15678
- }
15679
- return mcpServers;
15776
+ function nameToUriParts(name) {
15777
+ return nameToUriPart(name)
15778
+ .split('-')
15779
+ .filter((value) => value !== '');
15680
15780
  }
15681
15781
 
15682
15782
  /**
15683
- * Transpiler to Javascript code using OpenAI SDK.
15783
+ * Normalizes a given text to PascalCase format.
15684
15784
  *
15685
- * @public exported from `@promptbook/core`
15785
+ * Note: [🔂] This function is idempotent.
15786
+ *
15787
+ * @param text @public exported from `@promptbook/utils`
15788
+ * @returns
15789
+ * @example 'HelloWorld'
15790
+ * @example 'ILovePromptbook'
15791
+ * @public exported from `@promptbook/utils`
15686
15792
  */
15687
- const OpenAiSdkTranspiler = {
15688
- name: 'openai-sdk',
15689
- title: 'OpenAI SDK',
15690
- packageName: '@promptbook/core',
15691
- className: 'OpenAiSdkTranspiler',
15692
- async transpileBook(book, tools, options) {
15793
+ function normalizeTo_PascalCase(text) {
15794
+ return normalizeTo_camelCase(text, true);
15795
+ }
15796
+
15797
+ /**
15798
+ * Take every whitespace (space, new line, tab) and replace it with a single space
15799
+ *
15800
+ * Note: [🔂] This function is idempotent.
15801
+ *
15802
+ * @public exported from `@promptbook/utils`
15803
+ */
15804
+ function normalizeWhitespaces(sentence) {
15805
+ return sentence.replace(/\s+/gs, ' ').trim();
15806
+ }
15807
+
15808
+ /**
15809
+ * Removes quotes from a string
15810
+ *
15811
+ * Note: [🔂] This function is idempotent.
15812
+ * Tip: This is very useful for post-processing of the result of the LLM model
15813
+ * Note: This function removes only the same quotes from the beginning and the end of the string
15814
+ * Note: There are two similar functions:
15815
+ * - `removeQuotes` which removes only bounding quotes
15816
+ * - `unwrapResult` which removes whole introduce sentence
15817
+ *
15818
+ * @param text optionally quoted text
15819
+ * @returns text without quotes
15820
+ * @public exported from `@promptbook/utils`
15821
+ */
15822
+ function removeQuotes(text) {
15823
+ if (text.startsWith('"') && text.endsWith('"')) {
15824
+ return text.slice(1, -1);
15825
+ }
15826
+ if (text.startsWith("'") && text.endsWith("'")) {
15827
+ return text.slice(1, -1);
15828
+ }
15829
+ return text;
15830
+ }
15831
+
15832
+ /**
15833
+ * Removes quotes and optional introduce text from a string
15834
+ *
15835
+ * Tip: This is very useful for post-processing of the result of the LLM model
15836
+ * Note: This function trims the text and removes whole introduce sentence if it is present
15837
+ * Note: There are two similar functions:
15838
+ * - `removeQuotes` which removes only bounding quotes
15839
+ * - `unwrapResult` which removes whole introduce sentence
15840
+ *
15841
+ * @param text optionally quoted text
15842
+ * @returns text without quotes
15843
+ * @public exported from `@promptbook/utils`
15844
+ */
15845
+ function unwrapResult(text, options) {
15846
+ const { isTrimmed = true, isIntroduceSentenceRemoved = true } = options || {};
15847
+ let trimmedText = text;
15848
+ // Remove leading and trailing spaces and newlines
15849
+ if (isTrimmed) {
15850
+ trimmedText = spaceTrim.spaceTrim(trimmedText);
15851
+ }
15852
+ let processedText = trimmedText;
15853
+ if (isIntroduceSentenceRemoved) {
15854
+ const introduceSentenceRegex = /^[a-zěščřžýáíéúů:\s]*:\s*/i;
15855
+ if (introduceSentenceRegex.test(text)) {
15856
+ // Remove the introduce sentence and quotes by replacing it with an empty string
15857
+ processedText = processedText.replace(introduceSentenceRegex, '');
15858
+ }
15859
+ processedText = spaceTrim.spaceTrim(processedText);
15860
+ }
15861
+ if (processedText.length < 3) {
15862
+ return trimmedText;
15863
+ }
15864
+ if (processedText.includes('\n')) {
15865
+ return trimmedText;
15866
+ }
15867
+ // Remove the quotes by extracting the substring without the first and last characters
15868
+ const unquotedText = processedText.slice(1, -1);
15869
+ // Check if the text starts and ends with quotes
15870
+ if ([
15871
+ ['"', '"'],
15872
+ ["'", "'"],
15873
+ ['`', '`'],
15874
+ ['*', '*'],
15875
+ ['_', '_'],
15876
+ ['„', '“'],
15877
+ ['«', '»'] /* <- QUOTES to config */,
15878
+ ].some(([startQuote, endQuote]) => {
15879
+ if (!processedText.startsWith(startQuote)) {
15880
+ return false;
15881
+ }
15882
+ if (!processedText.endsWith(endQuote)) {
15883
+ return false;
15884
+ }
15885
+ if (unquotedText.includes(startQuote) && !unquotedText.includes(endQuote)) {
15886
+ return false;
15887
+ }
15888
+ if (!unquotedText.includes(startQuote) && unquotedText.includes(endQuote)) {
15889
+ return false;
15890
+ }
15891
+ return true;
15892
+ })) {
15893
+ return unwrapResult(unquotedText, { isTrimmed: false, isIntroduceSentenceRemoved: false });
15894
+ }
15895
+ else {
15896
+ return processedText;
15897
+ }
15898
+ }
15899
+ /**
15900
+ * TODO: [🧠] Should this also unwrap the (parenthesis)
15901
+ */
15902
+
15903
+ // <- TODO: Auto convert to type `import { ... } from 'type-fest';`
15904
+ /**
15905
+ * Tests if the value is [🚉] serializable as JSON
15906
+ *
15907
+ * - Almost all primitives are serializable BUT:
15908
+ * - `undefined` is not serializable
15909
+ * - `NaN` is not serializable
15910
+ * - Objects and arrays are serializable if all their properties are serializable
15911
+ * - Functions are not serializable
15912
+ * - Circular references are not serializable
15913
+ * - `Date` objects are not serializable
15914
+ * - `Map` and `Set` objects are not serializable
15915
+ * - `RegExp` objects are not serializable
15916
+ * - `Error` objects are not serializable
15917
+ * - `Symbol` objects are not serializable
15918
+ * - And much more...
15919
+ *
15920
+ *
15921
+ * @public exported from `@promptbook/utils`
15922
+ */
15923
+ function isSerializableAsJson(value) {
15924
+ try {
15925
+ checkSerializableAsJson({ value });
15926
+ return true;
15927
+ }
15928
+ catch (error) {
15929
+ return false;
15930
+ }
15931
+ }
15932
+ /**
15933
+ * TODO: [🧠][main] !!3 In-memory cache of same values to prevent multiple checks
15934
+ * TODO: [🧠][💺] Can be done this on type-level?
15935
+ */
15936
+
15937
+ /**
15938
+ * Determines if the given path is a root path.
15939
+ *
15940
+ * Note: This does not check if the file exists only if the path is valid
15941
+ * @public exported from `@promptbook/utils`
15942
+ */
15943
+ function isRootPath(value) {
15944
+ if (value === '/') {
15945
+ return true;
15946
+ }
15947
+ if (/^[A-Z]:\\$/i.test(value)) {
15948
+ return true;
15949
+ }
15950
+ return false;
15951
+ }
15952
+ /**
15953
+ * TODO: [🍏] Make for MacOS paths
15954
+ */
15955
+
15956
+ /**
15957
+ * Checks if the given value is a valid JavaScript identifier name.
15958
+ *
15959
+ * @param javascriptName The value to check for JavaScript identifier validity.
15960
+ * @returns `true` if the value is a valid JavaScript name, false otherwise.
15961
+ * @public exported from `@promptbook/utils`
15962
+ */
15963
+ function isValidJavascriptName(javascriptName) {
15964
+ if (typeof javascriptName !== 'string') {
15965
+ return false;
15966
+ }
15967
+ return /^[a-zA-Z_$][0-9a-zA-Z_$]*$/i.test(javascriptName);
15968
+ }
15969
+
15970
+ /**
15971
+ * Normalizes agent name from arbitrary string to valid agent name
15972
+ *
15973
+ * Note: [🔂] This function is idempotent.
15974
+ *
15975
+ * @public exported from `@promptbook/core`
15976
+ */
15977
+ function normalizeAgentName(rawAgentName) {
15978
+ return titleToName(spaceTrim__default["default"](rawAgentName));
15979
+ }
15980
+
15981
+ /**
15982
+ * Creates temporary default agent name based on agent source hash
15983
+ *
15984
+ * @public exported from `@promptbook/core`
15985
+ */
15986
+ function createDefaultAgentName(agentSource) {
15987
+ const agentHash = computeAgentHash(agentSource);
15988
+ return normalizeAgentName(`Agent ${agentHash.substring(0, 6)}`);
15989
+ }
15990
+
15991
+ /**
15992
+ * Parses basic information from agent source
15993
+ *
15994
+ * There are 2 similar functions:
15995
+ * - `parseAgentSource` which is a lightweight parser for agent source, it parses basic information and its purpose is to be quick and synchronous. The commitments there are hardcoded.
15996
+ * - `createAgentModelRequirements` which is an asynchronous function that creates model requirements it applies each commitment one by one and works asynchronously.
15997
+ *
15998
+ * @public exported from `@promptbook/core`
15999
+ */
16000
+ function parseAgentSource(agentSource) {
16001
+ const parseResult = parseAgentSourceWithCommitments(agentSource);
16002
+ // Find PERSONA and META commitments
16003
+ let personaDescription = null;
16004
+ for (const commitment of parseResult.commitments) {
16005
+ if (commitment.type !== 'PERSONA') {
16006
+ continue;
16007
+ }
16008
+ if (personaDescription === null) {
16009
+ personaDescription = '';
16010
+ }
16011
+ else {
16012
+ personaDescription += `\n\n${personaDescription}`;
16013
+ }
16014
+ personaDescription += commitment.content;
16015
+ }
16016
+ const meta = {};
16017
+ for (const commitment of parseResult.commitments) {
16018
+ if (commitment.type !== 'META') {
16019
+ continue;
16020
+ }
16021
+ // Parse META commitments - format is "META TYPE content"
16022
+ const metaTypeRaw = commitment.content.split(' ')[0] || 'NONE';
16023
+ const metaType = normalizeTo_camelCase(metaTypeRaw);
16024
+ meta[metaType] = spaceTrim__default["default"](commitment.content.substring(metaTypeRaw.length));
16025
+ }
16026
+ // Generate gravatar fallback if no meta image specified
16027
+ if (!meta.image) {
16028
+ meta.image = generatePlaceholderAgentProfileImageUrl(parseResult.agentName || '!!');
16029
+ }
16030
+ // Parse parameters using unified approach - both @Parameter and {parameter} notations
16031
+ // are treated as the same syntax feature with unified representation
16032
+ const parameters = parseParameters(agentSource);
16033
+ const agentHash = computeAgentHash(agentSource);
16034
+ return {
16035
+ agentName: normalizeAgentName(parseResult.agentName || createDefaultAgentName(agentSource)),
16036
+ agentHash,
16037
+ personaDescription,
16038
+ meta,
16039
+ parameters,
16040
+ };
16041
+ }
16042
+ /**
16043
+ * TODO: [🕛] Unite `AgentBasicInformation`, `ChatParticipant`, `LlmExecutionTools` + `LlmToolsMetadata`
16044
+ */
16045
+
16046
+ /**
16047
+ * Creates model requirements for an agent based on its source
16048
+ *
16049
+ * There are 2 similar functions:
16050
+ * - `parseAgentSource` which is a lightweight parser for agent source, it parses basic information and its purpose is to be quick and synchronous. The commitments there are hardcoded.
16051
+ * - `createAgentModelRequirements` which is an asynchronous function that creates model requirements it applies each commitment one by one and works asynchronous.
16052
+ *
16053
+ * @public exported from `@promptbook/core`
16054
+ */
16055
+ async function createAgentModelRequirements(agentSource, modelName, availableModels, llmTools) {
16056
+ // If availableModels are provided and no specific modelName is given,
16057
+ // use preparePersona to select the best model
16058
+ if (availableModels && !modelName && llmTools) {
16059
+ const selectedModelName = await selectBestModelUsingPersona(agentSource, llmTools);
16060
+ return createAgentModelRequirementsWithCommitments(agentSource, selectedModelName);
16061
+ }
16062
+ // Use the new commitment-based system with provided or default model
16063
+ return createAgentModelRequirementsWithCommitments(agentSource, modelName);
16064
+ }
16065
+ /**
16066
+ * Selects the best model using the preparePersona function
16067
+ * This directly uses preparePersona to ensure DRY principle
16068
+ *
16069
+ * @param agentSource The agent source to derive persona description from
16070
+ * @param llmTools LLM tools for preparing persona
16071
+ * @returns The name of the best selected model
16072
+ * @private function of `createAgentModelRequirements`
16073
+ */
16074
+ async function selectBestModelUsingPersona(agentSource, llmTools) {
16075
+ var _a;
16076
+ // Parse agent source to get persona description
16077
+ const { agentName, personaDescription } = parseAgentSource(agentSource);
16078
+ // Use agent name as fallback if no persona description is available
16079
+ const description = personaDescription || agentName || 'AI Agent';
16080
+ try {
16081
+ // Use preparePersona directly
16082
+ const { modelsRequirements } = await preparePersona(description, { llm: llmTools }, { isVerbose: false });
16083
+ // Extract the first model name from the requirements
16084
+ if (modelsRequirements.length > 0 && ((_a = modelsRequirements[0]) === null || _a === void 0 ? void 0 : _a.modelName)) {
16085
+ return modelsRequirements[0].modelName;
16086
+ }
16087
+ // Fallback: get available models and return the first CHAT model
16088
+ const availableModels = await llmTools.listModels();
16089
+ const chatModels = availableModels.filter(({ modelVariant }) => modelVariant === 'CHAT');
16090
+ if (chatModels.length === 0) {
16091
+ throw new Error('No CHAT models available for agent model selection');
16092
+ }
16093
+ return chatModels[0].modelName;
16094
+ }
16095
+ catch (error) {
16096
+ console.warn('Failed to use preparePersona for model selection, falling back to first available model:', error);
16097
+ // Fallback: get available models and return the first CHAT model
16098
+ const availableModels = await llmTools.listModels();
16099
+ const chatModels = availableModels.filter(({ modelVariant }) => modelVariant === 'CHAT');
16100
+ if (chatModels.length === 0) {
16101
+ throw new Error('No CHAT models available for agent model selection');
16102
+ }
16103
+ return chatModels[0].modelName;
16104
+ }
16105
+ }
16106
+ /**
16107
+ * Extracts MCP servers from agent source
16108
+ *
16109
+ * @param agentSource The agent source string that may contain MCP lines
16110
+ * @returns Array of MCP server identifiers
16111
+ *
16112
+ * @private TODO: [🧠] Maybe should be public
16113
+ */
16114
+ function extractMcpServers(agentSource) {
16115
+ if (!agentSource) {
16116
+ return [];
16117
+ }
16118
+ const lines = agentSource.split('\n');
16119
+ const mcpRegex = /^\s*MCP\s+(.+)$/i;
16120
+ const mcpServers = [];
16121
+ // Look for MCP lines
16122
+ for (const line of lines) {
16123
+ const match = line.match(mcpRegex);
16124
+ if (match && match[1]) {
16125
+ mcpServers.push(match[1].trim());
16126
+ }
16127
+ }
16128
+ return mcpServers;
16129
+ }
16130
+
16131
+ /**
16132
+ * Transpiler to Javascript code using OpenAI SDK.
16133
+ *
16134
+ * @public exported from `@promptbook/core`
16135
+ */
16136
+ const OpenAiSdkTranspiler = {
16137
+ name: 'openai-sdk',
16138
+ title: 'OpenAI SDK',
16139
+ packageName: '@promptbook/core',
16140
+ className: 'OpenAiSdkTranspiler',
16141
+ async transpileBook(book, tools, options) {
15693
16142
  const { agentName } = await parseAgentSource(book);
15694
16143
  const modelRequirements = await createAgentModelRequirements(book);
15695
16144
  const { commitments } = parseAgentSourceWithCommitments(book);
@@ -15804,7 +16253,7 @@
15804
16253
  });
15805
16254
 
15806
16255
  const answer = response.choices[0].message.content;
15807
- console.log('\\n🧠 ${agentName}:', answer, '\\n');
16256
+ console.log('\\n🧠 ${agentName /* <- TODO: [🕛] There should be `agentFullname` not `agentName` */}:', answer, '\\n');
15808
16257
 
15809
16258
  chatHistory.push({ role: 'assistant', content: answer });
15810
16259
  promptUser();
@@ -15823,7 +16272,7 @@
15823
16272
 
15824
16273
  (async () => {
15825
16274
  await setupKnowledge();
15826
- console.log("🤖 Chat with ${agentName} (type 'exit' to quit)\\n");
16275
+ console.log("🤖 Chat with ${agentName /* <- TODO: [🕛] There should be `agentFullname` not `agentName` */} (type 'exit' to quit)\\n");
15827
16276
  promptUser();
15828
16277
  })();
15829
16278
  `);
@@ -15870,7 +16319,7 @@
15870
16319
  });
15871
16320
 
15872
16321
  const answer = response.choices[0].message.content;
15873
- console.log('\\n🧠 ${agentName}:', answer, '\\n');
16322
+ console.log('\\n🧠 ${agentName /* <- TODO: [🕛] There should be `agentFullname` not `agentName` */}:', answer, '\\n');
15874
16323
 
15875
16324
  chatHistory.push({ role: 'assistant', content: answer });
15876
16325
  promptUser();
@@ -15887,7 +16336,7 @@
15887
16336
  });
15888
16337
  }
15889
16338
 
15890
- console.log("🤖 Chat with ${agentName} (type 'exit' to quit)\\n");
16339
+ console.log("🤖 Chat with ${agentName /* <- TODO: [🕛] There should be `agentFullname` not `agentName` */} (type 'exit' to quit)\\n");
15891
16340
  promptUser();
15892
16341
 
15893
16342
  `);
@@ -15903,7 +16352,7 @@
15903
16352
  * @public exported from `@promptbook/wizard`
15904
16353
  * @public exported from `@promptbook/cli`
15905
16354
  *
15906
- * TODO: !!!! Which package should export this?
16355
+ * TODO: [🧠] Which package should export this?
15907
16356
  */
15908
16357
  const _OpenAiSdkTranspilerRegistration = $bookTranspilersRegister.register(OpenAiSdkTranspiler);
15909
16358
  /**
@@ -16176,25 +16625,6 @@
16176
16625
  return identification;
16177
16626
  }
16178
16627
 
16179
- /**
16180
- * Determines if the given path is a root path.
16181
- *
16182
- * Note: This does not check if the file exists only if the path is valid
16183
- * @public exported from `@promptbook/utils`
16184
- */
16185
- function isRootPath(value) {
16186
- if (value === '/') {
16187
- return true;
16188
- }
16189
- if (/^[A-Z]:\\$/i.test(value)) {
16190
- return true;
16191
- }
16192
- return false;
16193
- }
16194
- /**
16195
- * TODO: [🍏] Make for MacOS paths
16196
- */
16197
-
16198
16628
  /**
16199
16629
  * Path to the `.env` file which was used to configure LLM tools
16200
16630
  *
@@ -16460,41 +16890,7 @@
16460
16890
  }
16461
16891
  }
16462
16892
  /**
16463
- * TODO: Write file more securely - ensure that there can be no accidental overwriting of existing variables and other content
16464
- */
16465
-
16466
- // <- TODO: Auto convert to type `import { ... } from 'type-fest';`
16467
- /**
16468
- * Tests if the value is [🚉] serializable as JSON
16469
- *
16470
- * - Almost all primitives are serializable BUT:
16471
- * - `undefined` is not serializable
16472
- * - `NaN` is not serializable
16473
- * - Objects and arrays are serializable if all their properties are serializable
16474
- * - Functions are not serializable
16475
- * - Circular references are not serializable
16476
- * - `Date` objects are not serializable
16477
- * - `Map` and `Set` objects are not serializable
16478
- * - `RegExp` objects are not serializable
16479
- * - `Error` objects are not serializable
16480
- * - `Symbol` objects are not serializable
16481
- * - And much more...
16482
- *
16483
- *
16484
- * @public exported from `@promptbook/utils`
16485
- */
16486
- function isSerializableAsJson(value) {
16487
- try {
16488
- checkSerializableAsJson({ value });
16489
- return true;
16490
- }
16491
- catch (error) {
16492
- return false;
16493
- }
16494
- }
16495
- /**
16496
- * TODO: [🧠][main] !!3 In-memory cache of same values to prevent multiple checks
16497
- * TODO: [🧠][💺] Can be done this on type-level?
16893
+ * TODO: Write file more securely - ensure that there can be no accidental overwriting of existing variables and other content
16498
16894
  */
16499
16895
 
16500
16896
  /**
@@ -16772,582 +17168,323 @@
16772
17168
  });
16773
17169
  shouldCache = validationResult.isValid;
16774
17170
  if (!shouldCache && isVerbose) {
16775
- console.info('Not caching result that fails expectations/format validation for key:', key, {
16776
- content: promptResult.content,
16777
- expectations: prompt.expectations,
16778
- format: prompt.format,
16779
- validationError: (_a = validationResult.error) === null || _a === void 0 ? void 0 : _a.message,
16780
- });
16781
- }
16782
- }
16783
- catch (error) {
16784
- // If validation throws an unexpected error, don't cache
16785
- shouldCache = false;
16786
- if (isVerbose) {
16787
- console.info('Not caching result due to validation error for key:', key, {
16788
- content: promptResult.content,
16789
- validationError: error instanceof Error ? error.message : String(error),
16790
- });
16791
- }
16792
- }
16793
- }
16794
- if (shouldCache) {
16795
- await storage.setItem(key, {
16796
- date: $getCurrentDate(),
16797
- promptbookVersion: PROMPTBOOK_ENGINE_VERSION,
16798
- bookVersion: BOOK_LANGUAGE_VERSION,
16799
- prompt: {
16800
- ...prompt,
16801
- parameters: Object.entries(parameters).length === Object.entries(relevantParameters).length
16802
- ? parameters
16803
- : {
16804
- ...relevantParameters,
16805
- note: `<- Note: Only relevant parameters are stored in the cache`,
16806
- },
16807
- },
16808
- promptResult,
16809
- });
16810
- }
16811
- else if (isVerbose && isBasicFailedResult) {
16812
- console.info('Not caching failed result for key:', key, {
16813
- content: promptResult.content,
16814
- error: promptResult.error,
16815
- success: promptResult.success,
16816
- });
16817
- }
16818
- return promptResult;
16819
- };
16820
- if (llmTools.callChatModel !== undefined) {
16821
- proxyTools.callChatModel = async (prompt) => {
16822
- return /* not await */ callCommonModel(prompt);
16823
- };
16824
- }
16825
- if (llmTools.callCompletionModel !== undefined) {
16826
- proxyTools.callCompletionModel = async (prompt) => {
16827
- return /* not await */ callCommonModel(prompt);
16828
- };
16829
- }
16830
- if (llmTools.callEmbeddingModel !== undefined) {
16831
- proxyTools.callEmbeddingModel = async (prompt) => {
16832
- return /* not await */ callCommonModel(prompt);
16833
- };
16834
- }
16835
- // <- Note: [🤖]
16836
- return proxyTools;
16837
- }
16838
- /**
16839
- * TODO: [🧠][💸] Maybe make some common abstraction `interceptLlmTools` and use here (or use javascript Proxy?)
16840
- * TODO: [🧠] Is there some meaningfull way how to test this util
16841
- * TODO: [👷‍♂️] Comprehensive manual about construction of llmTools
16842
- * Detailed explanation about caching strategies and appropriate storage selection for different use cases
16843
- * Examples of how to combine multiple interceptors for advanced caching, logging, and usage tracking
16844
- */
16845
-
16846
- /**
16847
- * Provides LLM tools configuration by reading environment variables.
16848
- *
16849
- * Note: `$` is used to indicate that this function is not a pure function - it uses filesystem to access `.env` file
16850
- *
16851
- * It looks for environment variables:
16852
- * - `process.env.OPENAI_API_KEY`
16853
- * - `process.env.ANTHROPIC_CLAUDE_API_KEY`
16854
- * - ...
16855
- *
16856
- * @see Environment variables documentation or .env file for required variables.
16857
- * @returns A promise that resolves to the LLM tools configuration, or null if configuration is incomplete or missing.
16858
- * @public exported from `@promptbook/node`
16859
- */
16860
- async function $provideLlmToolsConfigurationFromEnv() {
16861
- if (!$isRunningInNode()) {
16862
- throw new EnvironmentMismatchError('Function `$provideLlmToolsFromEnv` works only in Node.js environment');
16863
- }
16864
- const envFilepath = await $provideEnvFilename();
16865
- if (envFilepath !== null) {
16866
- dotenv__namespace.config({ path: envFilepath });
16867
- }
16868
- const llmToolsConfiguration = $llmToolsMetadataRegister
16869
- .list()
16870
- .map((metadata) => metadata.createConfigurationFromEnv(process.env))
16871
- .filter((configuration) => configuration !== null);
16872
- return llmToolsConfiguration;
16873
- }
16874
- /**
16875
- * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
16876
- */
16877
-
16878
- /**
16879
- * Creates LLM execution tools from provided configuration objects
16880
- *
16881
- * Instantiates and configures LLM tool instances for each configuration entry,
16882
- * combining them into a unified interface via MultipleLlmExecutionTools.
16883
- *
16884
- * Note: This function is not cached, every call creates new instance of `MultipleLlmExecutionTools`
16885
- *
16886
- * @param configuration Array of LLM tool configurations to instantiate
16887
- * @param options Additional options for configuring the LLM tools
16888
- * @returns A unified interface combining all successfully instantiated LLM tools
16889
- * @public exported from `@promptbook/core`
16890
- */
16891
- function createLlmToolsFromConfiguration(configuration, options = {}) {
16892
- const { title = 'LLM Tools from Configuration', isVerbose = DEFAULT_IS_VERBOSE, userId } = options;
16893
- const llmTools = configuration.map((llmConfiguration) => {
16894
- const registeredItem = $llmToolsRegister
16895
- .list()
16896
- .find(({ packageName, className }) => llmConfiguration.packageName === packageName && llmConfiguration.className === className);
16897
- if (registeredItem === undefined) {
16898
- // console.log('$llmToolsRegister.list()', $llmToolsRegister.list());
16899
- throw new Error(spaceTrim__default["default"]((block) => `
16900
- There is no constructor for LLM provider \`${llmConfiguration.className}\` from \`${llmConfiguration.packageName}\`
16901
- Running in ${!$isRunningInBrowser() ? '' : 'browser environment'}${!$isRunningInNode() ? '' : 'node environment'}${!$isRunningInWebWorker() ? '' : 'worker environment'}
16902
-
16903
- You have probably forgotten install and import the provider package.
16904
- To fix this issue, you can:
16905
-
16906
- Install:
16907
-
16908
- > npm install ${llmConfiguration.packageName}
16909
-
16910
- And import:
16911
-
16912
- > import '${llmConfiguration.packageName}';
16913
-
16914
-
16915
- ${block($registeredLlmToolsMessage())}
16916
- `));
16917
- }
16918
- return registeredItem({
16919
- isVerbose,
16920
- userId,
16921
- ...llmConfiguration.options,
16922
- });
16923
- });
16924
- return joinLlmExecutionTools(title, ...llmTools);
16925
- }
16926
- /**
16927
- * TODO: [🎌] Together with `createLlmToolsFromConfiguration` + 'EXECUTION_TOOLS_CLASSES' gets to `@promptbook/core` ALL model providers, make this more efficient
16928
- * TODO: [🧠][🎌] Dynamically install required providers
16929
- * TODO: We should implement an interactive configuration wizard that would:
16930
- * 1. Detect which LLM providers are available in the environment
16931
- * 2. Guide users through required configuration settings for each provider
16932
- * 3. Allow testing connections before completing setup
16933
- * 4. Generate appropriate configuration code for application integration
16934
- * TODO: [🧠][🍛] Which name is better `createLlmToolsFromConfig` or `createLlmToolsFromConfiguration`?
16935
- * TODO: [🧠] Is there some meaningfull way how to test this util
16936
- * TODO: This should be maybe not under `_common` but under `utils`
16937
- * TODO: [®] DRY Register logic
16938
- */
16939
-
16940
- /**
16941
- * Automatically configures LLM tools from environment variables in Node.js
16942
- *
16943
- * This utility function detects available LLM providers based on environment variables
16944
- * and creates properly configured LLM execution tools for each detected provider.
16945
- *
16946
- * Note: This function is not cached, every call creates new instance of `MultipleLlmExecutionTools`
16947
- *
16948
- * Supports environment variables from .env files when dotenv is configured
16949
- * Note: `$` is used to indicate that this function is not a pure function - it uses filesystem to access `.env` file
16950
- *
16951
- * It looks for environment variables:
16952
- * - `process.env.OPENAI_API_KEY`
16953
- * - `process.env.ANTHROPIC_CLAUDE_API_KEY`
16954
- * - ...
16955
- *
16956
- * @param options Configuration options for the LLM tools
16957
- * @returns A unified interface containing all detected and configured LLM tools
16958
- * @public exported from `@promptbook/node`
16959
- */
16960
- async function $provideLlmToolsFromEnv(options = {}) {
16961
- if (!$isRunningInNode()) {
16962
- throw new EnvironmentMismatchError('Function `$provideLlmToolsFromEnv` works only in Node.js environment');
16963
- }
16964
- const configuration = await $provideLlmToolsConfigurationFromEnv();
16965
- if (configuration.length === 0) {
16966
- if ($llmToolsMetadataRegister.list().length === 0) {
16967
- throw new UnexpectedError(spaceTrim__default["default"]((block) => `
16968
- No LLM tools registered, this is probably a bug in the Promptbook library
16969
-
16970
- ${block($registeredLlmToolsMessage())}}
16971
- `));
16972
- }
16973
- // TODO: [🥃]
16974
- throw new Error(spaceTrim__default["default"]((block) => `
16975
- No LLM tools found in the environment
16976
-
16977
- ${block($registeredLlmToolsMessage())}}
16978
- `));
16979
- }
16980
- return createLlmToolsFromConfiguration(configuration, options);
16981
- }
16982
- /**
16983
- * TODO: The architecture for LLM tools configuration consists of three key functions:
16984
- * 1. `$provideLlmToolsFromEnv` - High-level function that detects available providers from env vars and returns ready-to-use LLM tools
16985
- * 2. `$provideLlmToolsConfigurationFromEnv` - Middle layer that extracts configuration objects from environment variables
16986
- * 3. `createLlmToolsFromConfiguration` - Low-level function that instantiates LLM tools from explicit configuration
16987
- *
16988
- * This layered approach allows flexibility in how tools are configured:
16989
- * - Use $provideLlmToolsFromEnv for automatic detection and setup in Node.js environments
16990
- * - Use $provideLlmToolsConfigurationFromEnv to extract config objects for modification before instantiation
16991
- * - Use createLlmToolsFromConfiguration for explicit control over tool configurations
16992
- *
16993
- * TODO: [🧠][🍛] Which name is better `$provideLlmToolsFromEnv` or `$provideLlmToolsFromEnvironment`?
16994
- * TODO: [🧠] Is there some meaningfull way how to test this util
16995
- * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
16996
- * TODO: [🥃] Allow `ptbk make` without llm tools
16997
- * TODO: This should be maybe not under `_common` but under `utils`
16998
- * TODO: [®] DRY Register logic
16999
- */
17000
-
17001
- /**
17002
- * Returns LLM tools for CLI
17003
- *
17004
- * Note: `$` is used to indicate that this function is not a pure function - it uses filesystem to access `.env` file and also writes this .env file
17005
- *
17006
- * @private within the repository - for CLI utils
17007
- */
17008
- async function $provideLlmToolsForWizardOrCli(options) {
17009
- if (!$isRunningInNode()) {
17010
- throw new EnvironmentMismatchError('Function `$provideLlmToolsForWizardOrCli` works only in Node.js environment');
17011
- }
17012
- options = options !== null && options !== void 0 ? options : { strategy: 'BRING_YOUR_OWN_KEYS' };
17013
- const { isLoginloaded, strategy, isCacheReloaded } = options;
17014
- let llmExecutionTools;
17015
- if (strategy === 'REMOTE_SERVER') {
17016
- const { remoteServerUrl = DEFAULT_REMOTE_SERVER_URL, loginPrompt } = options;
17017
- const storage = new $EnvStorage();
17018
- let key = `PROMPTBOOK_TOKEN`;
17019
- if (remoteServerUrl !== DEFAULT_REMOTE_SERVER_URL) {
17020
- key = `${key}_${remoteServerUrl.replace(/^https?:\/\//i, '')}`;
17021
- }
17022
- let identification = null;
17023
- let promptbookToken = await storage.getItem(key);
17024
- if (promptbookToken === null || isLoginloaded) {
17025
- identification = await loginPrompt();
17026
- // Note: When login prompt fails, `process.exit(1)` is called so no need to check for null
17027
- if (identification.isAnonymous === false) {
17028
- promptbookToken = identificationToPromptbookToken(identification);
17029
- await storage.setItem(key, promptbookToken);
17171
+ console.info('Not caching result that fails expectations/format validation for key:', key, {
17172
+ content: promptResult.content,
17173
+ expectations: prompt.expectations,
17174
+ format: prompt.format,
17175
+ validationError: (_a = validationResult.error) === null || _a === void 0 ? void 0 : _a.message,
17176
+ });
17177
+ }
17178
+ }
17179
+ catch (error) {
17180
+ // If validation throws an unexpected error, don't cache
17181
+ shouldCache = false;
17182
+ if (isVerbose) {
17183
+ console.info('Not caching result due to validation error for key:', key, {
17184
+ content: promptResult.content,
17185
+ validationError: error instanceof Error ? error.message : String(error),
17186
+ });
17187
+ }
17030
17188
  }
17031
17189
  }
17032
- else {
17033
- identification = promptbookTokenToIdentification(promptbookToken);
17190
+ if (shouldCache) {
17191
+ await storage.setItem(key, {
17192
+ date: $getCurrentDate(),
17193
+ promptbookVersion: PROMPTBOOK_ENGINE_VERSION,
17194
+ bookVersion: BOOK_LANGUAGE_VERSION,
17195
+ prompt: {
17196
+ ...prompt,
17197
+ parameters: Object.entries(parameters).length === Object.entries(relevantParameters).length
17198
+ ? parameters
17199
+ : {
17200
+ ...relevantParameters,
17201
+ note: `<- Note: Only relevant parameters are stored in the cache`,
17202
+ },
17203
+ },
17204
+ promptResult,
17205
+ });
17034
17206
  }
17035
- llmExecutionTools = new RemoteLlmExecutionTools({
17036
- remoteServerUrl,
17037
- identification,
17038
- });
17207
+ else if (isVerbose && isBasicFailedResult) {
17208
+ console.info('Not caching failed result for key:', key, {
17209
+ content: promptResult.content,
17210
+ error: promptResult.error,
17211
+ success: promptResult.success,
17212
+ });
17213
+ }
17214
+ return promptResult;
17215
+ };
17216
+ if (llmTools.callChatModel !== undefined) {
17217
+ proxyTools.callChatModel = async (prompt) => {
17218
+ return /* not await */ callCommonModel(prompt);
17219
+ };
17039
17220
  }
17040
- else if (strategy === 'BRING_YOUR_OWN_KEYS') {
17041
- llmExecutionTools = await $provideLlmToolsFromEnv({
17042
- title: 'LLM Tools for wizard or CLI with BYOK strategy',
17043
- });
17221
+ if (llmTools.callCompletionModel !== undefined) {
17222
+ proxyTools.callCompletionModel = async (prompt) => {
17223
+ return /* not await */ callCommonModel(prompt);
17224
+ };
17044
17225
  }
17045
- else {
17046
- throw new UnexpectedError(`\`$provideLlmToolsForWizardOrCli\` wrong strategy "${strategy}"`);
17226
+ if (llmTools.callEmbeddingModel !== undefined) {
17227
+ proxyTools.callEmbeddingModel = async (prompt) => {
17228
+ return /* not await */ callCommonModel(prompt);
17229
+ };
17047
17230
  }
17048
- return cacheLlmTools(countUsage(
17049
- // <- TODO: [🌯] We dont use countUsage at all, maybe just unwrap it
17050
- // <- Note: for example here we don`t want the [🌯]
17051
- llmExecutionTools), {
17052
- storage: new FileCacheStorage({ fs: $provideFilesystemForNode() }, {
17053
- rootFolderPath: path.join(process.cwd(), DEFAULT_EXECUTION_CACHE_DIRNAME),
17054
- }),
17055
- isCacheReloaded,
17056
- });
17231
+ // <- Note: [🤖]
17232
+ return proxyTools;
17057
17233
  }
17058
17234
  /**
17059
- * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
17060
- * TODO: [👷‍♂️] @@@ Manual about construction of llmTools
17061
- * TODO: [🥃] Allow `ptbk make` without llm tools
17062
- * TODO: This should be maybe not under `_common` but under `utils-internal` / `utils/internal`
17063
- * TODO: [®] DRY Register logic
17235
+ * TODO: [🧠][💸] Maybe make some common abstraction `interceptLlmTools` and use here (or use javascript Proxy?)
17236
+ * TODO: [🧠] Is there some meaningfull way how to test this util
17237
+ * TODO: [👷‍♂️] Comprehensive manual about construction of llmTools
17238
+ * Detailed explanation about caching strategies and appropriate storage selection for different use cases
17239
+ * Examples of how to combine multiple interceptors for advanced caching, logging, and usage tracking
17064
17240
  */
17065
17241
 
17066
17242
  /**
17067
- * Provides a collection of scrapers optimized for Node.js environment.
17068
- * 1) `provideScrapersForNode` use as default
17069
- * 2) `provideScrapersForBrowser` use in limited browser environment *
17243
+ * Provides LLM tools configuration by reading environment variables.
17244
+ *
17245
+ * Note: `$` is used to indicate that this function is not a pure function - it uses filesystem to access `.env` file
17246
+ *
17247
+ * It looks for environment variables:
17248
+ * - `process.env.OPENAI_API_KEY`
17249
+ * - `process.env.ANTHROPIC_CLAUDE_API_KEY`
17250
+ * - ...
17251
+ *
17252
+ * @see Environment variables documentation or .env file for required variables.
17253
+ * @returns A promise that resolves to the LLM tools configuration, or null if configuration is incomplete or missing.
17070
17254
  * @public exported from `@promptbook/node`
17071
17255
  */
17072
- async function $provideScrapersForNode(tools, options) {
17256
+ async function $provideLlmToolsConfigurationFromEnv() {
17073
17257
  if (!$isRunningInNode()) {
17074
- throw new EnvironmentMismatchError('Function `$getScrapersForNode` works only in Node.js environment');
17258
+ throw new EnvironmentMismatchError('Function `$provideLlmToolsFromEnv` works only in Node.js environment');
17075
17259
  }
17076
- // TODO: [🔱] Do here auto-installation + auto-include of missing scrapers - use all from $scrapersMetadataRegister.list()
17077
- // TODO: [🔱][🧠] What is the best strategy for auto-install - install them all?
17078
- const scrapers = [];
17079
- for (const scraperFactory of $scrapersRegister.list()) {
17080
- const scraper = await scraperFactory(tools, options || {});
17081
- if (scraper.metadata.packageName === '@promptbook/boilerplate' ||
17082
- scraper.metadata.mimeTypes.some((mimeType) => mimeType.includes('DISABLED'))) {
17083
- continue;
17084
- }
17085
- scrapers.push(scraper);
17260
+ const envFilepath = await $provideEnvFilename();
17261
+ if (envFilepath !== null) {
17262
+ dotenv__namespace.config({ path: envFilepath });
17086
17263
  }
17087
- return scrapers;
17264
+ const llmToolsConfiguration = $llmToolsMetadataRegister
17265
+ .list()
17266
+ .map((metadata) => metadata.createConfigurationFromEnv(process.env))
17267
+ .filter((configuration) => configuration !== null);
17268
+ return llmToolsConfiguration;
17088
17269
  }
17089
17270
  /**
17090
17271
  * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
17091
17272
  */
17092
17273
 
17093
17274
  /**
17094
- * Function parseNumber will parse number from string
17275
+ * Creates LLM execution tools from provided configuration objects
17095
17276
  *
17096
- * Note: [🔂] This function is idempotent.
17097
- * Unlike Number.parseInt, Number.parseFloat it will never ever result in NaN
17098
- * Note: it also works only with decimal numbers
17277
+ * Instantiates and configures LLM tool instances for each configuration entry,
17278
+ * combining them into a unified interface via MultipleLlmExecutionTools.
17099
17279
  *
17100
- * @returns parsed number
17101
- * @throws {ParseError} if the value is not a number
17280
+ * Note: This function is not cached, every call creates new instance of `MultipleLlmExecutionTools`
17102
17281
  *
17103
- * @public exported from `@promptbook/utils`
17104
- */
17105
- function parseNumber(value) {
17106
- const originalValue = value;
17107
- if (typeof value === 'number') {
17108
- value = value.toString(); // <- TODO: Maybe more efficient way to do this
17109
- }
17110
- if (typeof value !== 'string') {
17111
- return 0;
17112
- }
17113
- value = value.trim();
17114
- if (value.startsWith('+')) {
17115
- return parseNumber(value.substring(1));
17116
- }
17117
- if (value.startsWith('-')) {
17118
- const number = parseNumber(value.substring(1));
17119
- if (number === 0) {
17120
- return 0; // <- Note: To prevent -0
17121
- }
17122
- return -number;
17123
- }
17124
- value = value.replace(/,/g, '.');
17125
- value = value.toUpperCase();
17126
- if (value === '') {
17127
- return 0;
17128
- }
17129
- if (value === '♾' || value.startsWith('INF')) {
17130
- return Infinity;
17131
- }
17132
- if (value.includes('/')) {
17133
- const [numerator_, denominator_] = value.split('/');
17134
- const numerator = parseNumber(numerator_);
17135
- const denominator = parseNumber(denominator_);
17136
- if (denominator === 0) {
17137
- throw new ParseError(`Unable to parse number from "${originalValue}" because denominator is zero`);
17138
- }
17139
- return numerator / denominator;
17140
- }
17141
- if (/^(NAN|NULL|NONE|UNDEFINED|ZERO|NO.*)$/.test(value)) {
17142
- return 0;
17143
- }
17144
- if (value.includes('E')) {
17145
- const [significand, exponent] = value.split('E');
17146
- return parseNumber(significand) * 10 ** parseNumber(exponent);
17147
- }
17148
- if (!/^[0-9.]+$/.test(value) || value.split('.').length > 2) {
17149
- throw new ParseError(`Unable to parse number from "${originalValue}"`);
17150
- }
17151
- const num = parseFloat(value);
17152
- if (isNaN(num)) {
17153
- throw new ParseError(`Unexpected NaN when parsing number from "${originalValue}"`);
17154
- }
17155
- return num;
17156
- }
17157
- /**
17158
- * TODO: Maybe use sth. like safe-eval in fraction/calculation case @see https://www.npmjs.com/package/safe-eval
17159
- * TODO: [🧠][🌻] Maybe export through `@promptbook/markdown-utils` not `@promptbook/utils`
17282
+ * @param configuration Array of LLM tool configurations to instantiate
17283
+ * @param options Additional options for configuring the LLM tools
17284
+ * @returns A unified interface combining all successfully instantiated LLM tools
17285
+ * @public exported from `@promptbook/core`
17160
17286
  */
17287
+ function createLlmToolsFromConfiguration(configuration, options = {}) {
17288
+ const { title = 'LLM Tools from Configuration', isVerbose = DEFAULT_IS_VERBOSE, userId } = options;
17289
+ const llmTools = configuration.map((llmConfiguration) => {
17290
+ const registeredItem = $llmToolsRegister
17291
+ .list()
17292
+ .find(({ packageName, className }) => llmConfiguration.packageName === packageName && llmConfiguration.className === className);
17293
+ if (registeredItem === undefined) {
17294
+ // console.log('$llmToolsRegister.list()', $llmToolsRegister.list());
17295
+ throw new Error(spaceTrim__default["default"]((block) => `
17296
+ There is no constructor for LLM provider \`${llmConfiguration.className}\` from \`${llmConfiguration.packageName}\`
17297
+ Running in ${!$isRunningInBrowser() ? '' : 'browser environment'}${!$isRunningInNode() ? '' : 'node environment'}${!$isRunningInWebWorker() ? '' : 'worker environment'}
17161
17298
 
17162
- /**
17163
- * Makes first letter of a string lowercase
17164
- *
17165
- * Note: [🔂] This function is idempotent.
17166
- *
17167
- * @public exported from `@promptbook/utils`
17168
- */
17169
- function decapitalize(word) {
17170
- return word.substring(0, 1).toLowerCase() + word.substring(1);
17171
- }
17299
+ You have probably forgotten install and import the provider package.
17300
+ To fix this issue, you can:
17172
17301
 
17173
- /**
17174
- * Parses keywords from a string
17175
- *
17176
- * @param {string} input
17177
- * @returns {Set} of keywords without diacritics in lowercase
17178
- * @public exported from `@promptbook/utils`
17179
- */
17180
- function parseKeywordsFromString(input) {
17181
- const keywords = normalizeTo_SCREAMING_CASE(removeDiacritics(input))
17182
- .toLowerCase()
17183
- .split(/[^a-z0-9]+/gs)
17184
- .filter((value) => value);
17185
- return new Set(keywords);
17186
- }
17302
+ Install:
17303
+
17304
+ > npm install ${llmConfiguration.packageName}
17305
+
17306
+ And import:
17307
+
17308
+ > import '${llmConfiguration.packageName}';
17187
17309
 
17188
- /**
17189
- * Converts a name string into a URI-compatible format.
17190
- *
17191
- * @param name The string to be converted to a URI-compatible format.
17192
- * @returns A URI-compatible string derived from the input name.
17193
- * @example 'Hello World' -> 'hello-world'
17194
- * @public exported from `@promptbook/utils`
17195
- */
17196
- function nameToUriPart(name) {
17197
- let uriPart = name;
17198
- uriPart = uriPart.toLowerCase();
17199
- uriPart = removeDiacritics(uriPart);
17200
- uriPart = uriPart.replace(/[^a-zA-Z0-9]+/g, '-');
17201
- uriPart = uriPart.replace(/^-+/, '');
17202
- uriPart = uriPart.replace(/-+$/, '');
17203
- return uriPart;
17204
- }
17205
17310
 
17206
- /**
17207
- * Converts a given name into URI-compatible parts.
17208
- *
17209
- * @param name The name to be converted into URI parts.
17210
- * @returns An array of URI-compatible parts derived from the name.
17211
- * @example 'Example Name' -> ['example', 'name']
17212
- * @public exported from `@promptbook/utils`
17213
- */
17214
- function nameToUriParts(name) {
17215
- return nameToUriPart(name)
17216
- .split('-')
17217
- .filter((value) => value !== '');
17311
+ ${block($registeredLlmToolsMessage())}
17312
+ `));
17313
+ }
17314
+ return registeredItem({
17315
+ isVerbose,
17316
+ userId,
17317
+ ...llmConfiguration.options,
17318
+ });
17319
+ });
17320
+ return joinLlmExecutionTools(title, ...llmTools);
17218
17321
  }
17219
-
17220
17322
  /**
17221
- *
17222
- * @param text @public exported from `@promptbook/utils`
17223
- * @returns
17224
- * @example 'HelloWorld'
17225
- * @example 'ILovePromptbook'
17226
- * @public exported from `@promptbook/utils`
17323
+ * TODO: [🎌] Together with `createLlmToolsFromConfiguration` + 'EXECUTION_TOOLS_CLASSES' gets to `@promptbook/core` ALL model providers, make this more efficient
17324
+ * TODO: [🧠][🎌] Dynamically install required providers
17325
+ * TODO: We should implement an interactive configuration wizard that would:
17326
+ * 1. Detect which LLM providers are available in the environment
17327
+ * 2. Guide users through required configuration settings for each provider
17328
+ * 3. Allow testing connections before completing setup
17329
+ * 4. Generate appropriate configuration code for application integration
17330
+ * TODO: [🧠][🍛] Which name is better `createLlmToolsFromConfig` or `createLlmToolsFromConfiguration`?
17331
+ * TODO: [🧠] Is there some meaningfull way how to test this util
17332
+ * TODO: This should be maybe not under `_common` but under `utils`
17333
+ * TODO: [®] DRY Register logic
17227
17334
  */
17228
- function normalizeTo_PascalCase(text) {
17229
- return normalizeTo_camelCase(text, true);
17230
- }
17231
17335
 
17232
17336
  /**
17233
- * Take every whitespace (space, new line, tab) and replace it with a single space
17337
+ * Automatically configures LLM tools from environment variables in Node.js
17234
17338
  *
17235
- * Note: [🔂] This function is idempotent.
17339
+ * This utility function detects available LLM providers based on environment variables
17340
+ * and creates properly configured LLM execution tools for each detected provider.
17236
17341
  *
17237
- * @public exported from `@promptbook/utils`
17238
- */
17239
- function normalizeWhitespaces(sentence) {
17240
- return sentence.replace(/\s+/gs, ' ').trim();
17241
- }
17242
-
17243
- /**
17244
- * Removes quotes from a string
17342
+ * Note: This function is not cached, every call creates new instance of `MultipleLlmExecutionTools`
17245
17343
  *
17246
- * Note: [🔂] This function is idempotent.
17247
- * Tip: This is very useful for post-processing of the result of the LLM model
17248
- * Note: This function removes only the same quotes from the beginning and the end of the string
17249
- * Note: There are two similar functions:
17250
- * - `removeQuotes` which removes only bounding quotes
17251
- * - `unwrapResult` which removes whole introduce sentence
17344
+ * Supports environment variables from .env files when dotenv is configured
17345
+ * Note: `$` is used to indicate that this function is not a pure function - it uses filesystem to access `.env` file
17252
17346
  *
17253
- * @param text optionally quoted text
17254
- * @returns text without quotes
17255
- * @public exported from `@promptbook/utils`
17347
+ * It looks for environment variables:
17348
+ * - `process.env.OPENAI_API_KEY`
17349
+ * - `process.env.ANTHROPIC_CLAUDE_API_KEY`
17350
+ * - ...
17351
+ *
17352
+ * @param options Configuration options for the LLM tools
17353
+ * @returns A unified interface containing all detected and configured LLM tools
17354
+ * @public exported from `@promptbook/node`
17256
17355
  */
17257
- function removeQuotes(text) {
17258
- if (text.startsWith('"') && text.endsWith('"')) {
17259
- return text.slice(1, -1);
17356
+ async function $provideLlmToolsFromEnv(options = {}) {
17357
+ if (!$isRunningInNode()) {
17358
+ throw new EnvironmentMismatchError('Function `$provideLlmToolsFromEnv` works only in Node.js environment');
17260
17359
  }
17261
- if (text.startsWith("'") && text.endsWith("'")) {
17262
- return text.slice(1, -1);
17360
+ const configuration = await $provideLlmToolsConfigurationFromEnv();
17361
+ if (configuration.length === 0) {
17362
+ if ($llmToolsMetadataRegister.list().length === 0) {
17363
+ throw new UnexpectedError(spaceTrim__default["default"]((block) => `
17364
+ No LLM tools registered, this is probably a bug in the Promptbook library
17365
+
17366
+ ${block($registeredLlmToolsMessage())}}
17367
+ `));
17368
+ }
17369
+ // TODO: [🥃]
17370
+ throw new Error(spaceTrim__default["default"]((block) => `
17371
+ No LLM tools found in the environment
17372
+
17373
+ ${block($registeredLlmToolsMessage())}}
17374
+ `));
17263
17375
  }
17264
- return text;
17376
+ return createLlmToolsFromConfiguration(configuration, options);
17265
17377
  }
17378
+ /**
17379
+ * TODO: The architecture for LLM tools configuration consists of three key functions:
17380
+ * 1. `$provideLlmToolsFromEnv` - High-level function that detects available providers from env vars and returns ready-to-use LLM tools
17381
+ * 2. `$provideLlmToolsConfigurationFromEnv` - Middle layer that extracts configuration objects from environment variables
17382
+ * 3. `createLlmToolsFromConfiguration` - Low-level function that instantiates LLM tools from explicit configuration
17383
+ *
17384
+ * This layered approach allows flexibility in how tools are configured:
17385
+ * - Use $provideLlmToolsFromEnv for automatic detection and setup in Node.js environments
17386
+ * - Use $provideLlmToolsConfigurationFromEnv to extract config objects for modification before instantiation
17387
+ * - Use createLlmToolsFromConfiguration for explicit control over tool configurations
17388
+ *
17389
+ * TODO: [🧠][🍛] Which name is better `$provideLlmToolsFromEnv` or `$provideLlmToolsFromEnvironment`?
17390
+ * TODO: [🧠] Is there some meaningfull way how to test this util
17391
+ * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
17392
+ * TODO: [🥃] Allow `ptbk make` without llm tools
17393
+ * TODO: This should be maybe not under `_common` but under `utils`
17394
+ * TODO: [®] DRY Register logic
17395
+ */
17266
17396
 
17267
17397
  /**
17268
- * Removes quotes and optional introduce text from a string
17398
+ * Returns LLM tools for CLI
17269
17399
  *
17270
- * Tip: This is very useful for post-processing of the result of the LLM model
17271
- * Note: This function trims the text and removes whole introduce sentence if it is present
17272
- * Note: There are two similar functions:
17273
- * - `removeQuotes` which removes only bounding quotes
17274
- * - `unwrapResult` which removes whole introduce sentence
17400
+ * Note: `$` is used to indicate that this function is not a pure function - it uses filesystem to access `.env` file and also writes this .env file
17275
17401
  *
17276
- * @param text optionally quoted text
17277
- * @returns text without quotes
17278
- * @public exported from `@promptbook/utils`
17402
+ * @private within the repository - for CLI utils
17279
17403
  */
17280
- function unwrapResult(text, options) {
17281
- const { isTrimmed = true, isIntroduceSentenceRemoved = true } = options || {};
17282
- let trimmedText = text;
17283
- // Remove leading and trailing spaces and newlines
17284
- if (isTrimmed) {
17285
- trimmedText = spaceTrim.spaceTrim(trimmedText);
17286
- }
17287
- let processedText = trimmedText;
17288
- if (isIntroduceSentenceRemoved) {
17289
- const introduceSentenceRegex = /^[a-zěščřžýáíéúů:\s]*:\s*/i;
17290
- if (introduceSentenceRegex.test(text)) {
17291
- // Remove the introduce sentence and quotes by replacing it with an empty string
17292
- processedText = processedText.replace(introduceSentenceRegex, '');
17293
- }
17294
- processedText = spaceTrim.spaceTrim(processedText);
17295
- }
17296
- if (processedText.length < 3) {
17297
- return trimmedText;
17298
- }
17299
- if (processedText.includes('\n')) {
17300
- return trimmedText;
17404
+ async function $provideLlmToolsForWizardOrCli(options) {
17405
+ if (!$isRunningInNode()) {
17406
+ throw new EnvironmentMismatchError('Function `$provideLlmToolsForWizardOrCli` works only in Node.js environment');
17301
17407
  }
17302
- // Remove the quotes by extracting the substring without the first and last characters
17303
- const unquotedText = processedText.slice(1, -1);
17304
- // Check if the text starts and ends with quotes
17305
- if ([
17306
- ['"', '"'],
17307
- ["'", "'"],
17308
- ['`', '`'],
17309
- ['*', '*'],
17310
- ['_', '_'],
17311
- ['„', '“'],
17312
- ['«', '»'] /* <- QUOTES to config */,
17313
- ].some(([startQuote, endQuote]) => {
17314
- if (!processedText.startsWith(startQuote)) {
17315
- return false;
17316
- }
17317
- if (!processedText.endsWith(endQuote)) {
17318
- return false;
17408
+ options = options !== null && options !== void 0 ? options : { strategy: 'BRING_YOUR_OWN_KEYS' };
17409
+ const { isLoginloaded, strategy, isCacheReloaded } = options;
17410
+ let llmExecutionTools;
17411
+ if (strategy === 'REMOTE_SERVER') {
17412
+ const { remoteServerUrl = DEFAULT_REMOTE_SERVER_URL, loginPrompt } = options;
17413
+ const storage = new $EnvStorage();
17414
+ let key = `PROMPTBOOK_TOKEN`;
17415
+ if (remoteServerUrl !== DEFAULT_REMOTE_SERVER_URL) {
17416
+ key = `${key}_${remoteServerUrl.replace(/^https?:\/\//i, '')}`;
17319
17417
  }
17320
- if (unquotedText.includes(startQuote) && !unquotedText.includes(endQuote)) {
17321
- return false;
17418
+ let identification = null;
17419
+ let promptbookToken = await storage.getItem(key);
17420
+ if (promptbookToken === null || isLoginloaded) {
17421
+ identification = await loginPrompt();
17422
+ // Note: When login prompt fails, `process.exit(1)` is called so no need to check for null
17423
+ if (identification.isAnonymous === false) {
17424
+ promptbookToken = identificationToPromptbookToken(identification);
17425
+ await storage.setItem(key, promptbookToken);
17426
+ }
17322
17427
  }
17323
- if (!unquotedText.includes(startQuote) && unquotedText.includes(endQuote)) {
17324
- return false;
17428
+ else {
17429
+ identification = promptbookTokenToIdentification(promptbookToken);
17325
17430
  }
17326
- return true;
17327
- })) {
17328
- return unwrapResult(unquotedText, { isTrimmed: false, isIntroduceSentenceRemoved: false });
17431
+ llmExecutionTools = new RemoteLlmExecutionTools({
17432
+ remoteServerUrl,
17433
+ identification,
17434
+ });
17435
+ }
17436
+ else if (strategy === 'BRING_YOUR_OWN_KEYS') {
17437
+ llmExecutionTools = await $provideLlmToolsFromEnv({
17438
+ title: 'LLM Tools for wizard or CLI with BYOK strategy',
17439
+ });
17329
17440
  }
17330
17441
  else {
17331
- return processedText;
17442
+ throw new UnexpectedError(`\`$provideLlmToolsForWizardOrCli\` wrong strategy "${strategy}"`);
17332
17443
  }
17444
+ return cacheLlmTools(countUsage(
17445
+ // <- TODO: [🌯] We dont use countUsage at all, maybe just unwrap it
17446
+ // <- Note: for example here we don`t want the [🌯]
17447
+ llmExecutionTools), {
17448
+ storage: new FileCacheStorage({ fs: $provideFilesystemForNode() }, {
17449
+ rootFolderPath: path.join(process.cwd(), DEFAULT_EXECUTION_CACHE_DIRNAME),
17450
+ }),
17451
+ isCacheReloaded,
17452
+ });
17333
17453
  }
17334
17454
  /**
17335
- * TODO: [🧠] Should this also unwrap the (parenthesis)
17455
+ * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
17456
+ * TODO: [👷‍♂️] @@@ Manual about construction of llmTools
17457
+ * TODO: [🥃] Allow `ptbk make` without llm tools
17458
+ * TODO: This should be maybe not under `_common` but under `utils-internal` / `utils/internal`
17459
+ * TODO: [®] DRY Register logic
17336
17460
  */
17337
17461
 
17338
17462
  /**
17339
- * Checks if the given value is a valid JavaScript identifier name.
17340
- *
17341
- * @param javascriptName The value to check for JavaScript identifier validity.
17342
- * @returns `true` if the value is a valid JavaScript name, false otherwise.
17343
- * @public exported from `@promptbook/utils`
17463
+ * Provides a collection of scrapers optimized for Node.js environment.
17464
+ * 1) `provideScrapersForNode` use as default
17465
+ * 2) `provideScrapersForBrowser` use in limited browser environment *
17466
+ * @public exported from `@promptbook/node`
17344
17467
  */
17345
- function isValidJavascriptName(javascriptName) {
17346
- if (typeof javascriptName !== 'string') {
17347
- return false;
17468
+ async function $provideScrapersForNode(tools, options) {
17469
+ if (!$isRunningInNode()) {
17470
+ throw new EnvironmentMismatchError('Function `$getScrapersForNode` works only in Node.js environment');
17348
17471
  }
17349
- return /^[a-zA-Z_$][0-9a-zA-Z_$]*$/i.test(javascriptName);
17472
+ // TODO: [🔱] Do here auto-installation + auto-include of missing scrapers - use all from $scrapersMetadataRegister.list()
17473
+ // TODO: [🔱][🧠] What is the best strategy for auto-install - install them all?
17474
+ const scrapers = [];
17475
+ for (const scraperFactory of $scrapersRegister.list()) {
17476
+ const scraper = await scraperFactory(tools, options || {});
17477
+ if (scraper.metadata.packageName === '@promptbook/boilerplate' ||
17478
+ scraper.metadata.mimeTypes.some((mimeType) => mimeType.includes('DISABLED'))) {
17479
+ continue;
17480
+ }
17481
+ scrapers.push(scraper);
17482
+ }
17483
+ return scrapers;
17350
17484
  }
17485
+ /**
17486
+ * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
17487
+ */
17351
17488
 
17352
17489
  /**
17353
17490
  * Extracts exactly ONE code block from markdown.