@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/esm/index.es.js CHANGED
@@ -6,9 +6,9 @@ import Bottleneck from 'bottleneck';
6
6
  import colors from 'colors';
7
7
  import { OpenAIClient, AzureKeyCredential } from '@azure/openai';
8
8
  import OpenAI from 'openai';
9
- import { forEver, forTime } from 'waitasecond';
10
9
  import { mkdir, rm, readFile, readdir, rename, rmdir, stat, access, constants, writeFile, watch, unlink } from 'fs/promises';
11
10
  import { spawn } from 'child_process';
11
+ import { forTime } from 'waitasecond';
12
12
  import { SHA256 } from 'crypto-js';
13
13
  import hexEncoder from 'crypto-js/enc-hex';
14
14
  import { basename, join, dirname, isAbsolute, relative } from 'path';
@@ -36,7 +36,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
36
36
  * @generated
37
37
  * @see https://github.com/webgptorg/promptbook
38
38
  */
39
- const PROMPTBOOK_ENGINE_VERSION = '0.103.0-46';
39
+ const PROMPTBOOK_ENGINE_VERSION = '0.103.0-48';
40
40
  /**
41
41
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
42
42
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -49,15 +49,20 @@ const PROMPTBOOK_ENGINE_VERSION = '0.103.0-46';
49
49
  */
50
50
  const REMOTE_SERVER_URLS = [
51
51
  {
52
- title: 'Promptbook',
53
- description: `Servers of Promptbook.studio`,
52
+ title: 'Promptbook.Studio',
53
+ description: `Server of Promptbook.studio`,
54
54
  owner: 'AI Web, LLC <legal@ptbk.io> (https://www.ptbk.io/)',
55
- isAnonymousModeAllowed: true,
56
55
  urls: [
57
56
  'https://promptbook.s5.ptbk.io/',
58
57
  // Note: Servers 1-4 are not running
59
58
  ],
60
59
  },
60
+ {
61
+ title: 'Testing Agents',
62
+ description: `Testing Agents server on Vercel`,
63
+ owner: 'AI Web, LLC <legal@ptbk.io> (https://www.ptbk.io/)',
64
+ urls: ['https://s6.ptbk.io/'],
65
+ },
61
66
  /*
62
67
  Note: Working on older version of Promptbook and not supported anymore
63
68
  {
@@ -315,9 +320,6 @@ function checkChannelValue(channelName, value) {
315
320
  throw new Error(`${channelName} channel is greater than 255, it is ${value}`);
316
321
  }
317
322
  }
318
- /**
319
- * TODO: [🧠][🚓] Is/which combination it better to use asserts/check, validate or is utility function?
320
- */
321
323
 
322
324
  /**
323
325
  * Color object represents an RGB color with alpha channel
@@ -3857,17 +3859,17 @@ const OPENAI_MODELS = exportJson({
3857
3859
  },
3858
3860
  /**/
3859
3861
  /*/
3860
- {
3861
- modelTitle: 'tts-1-hd-1106',
3862
- modelName: 'tts-1-hd-1106',
3863
- },
3864
- /**/
3862
+ {
3863
+ modelTitle: 'tts-1-hd-1106',
3864
+ modelName: 'tts-1-hd-1106',
3865
+ },
3866
+ /**/
3865
3867
  /*/
3866
- {
3867
- modelTitle: 'tts-1-hd',
3868
- modelName: 'tts-1-hd',
3869
- },
3870
- /**/
3868
+ {
3869
+ modelTitle: 'tts-1-hd',
3870
+ modelName: 'tts-1-hd',
3871
+ },
3872
+ /**/
3871
3873
  /**/
3872
3874
  {
3873
3875
  modelVariant: 'CHAT',
@@ -5205,26 +5207,6 @@ const GOOGLE_MODELS = exportJson({
5205
5207
  output: pricing(`$0.45 / 1M tokens`),
5206
5208
  },
5207
5209
  },
5208
- {
5209
- modelVariant: 'CHAT',
5210
- modelTitle: 'Gemini 2.0 Flash',
5211
- modelName: 'gemini-2.0-flash',
5212
- 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.',
5213
- pricing: {
5214
- prompt: pricing(`$0.35 / 1M tokens`),
5215
- output: pricing(`$1.05 / 1M tokens`),
5216
- },
5217
- },
5218
- {
5219
- modelVariant: 'CHAT',
5220
- modelTitle: 'Gemini 2.0 Flash Lite',
5221
- modelName: 'gemini-2.0-flash-lite',
5222
- 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.',
5223
- pricing: {
5224
- prompt: pricing(`$0.20 / 1M tokens`),
5225
- output: pricing(`$0.60 / 1M tokens`),
5226
- },
5227
- },
5228
5210
  {
5229
5211
  modelVariant: 'CHAT',
5230
5212
  modelTitle: 'Gemini 2.0 Flash Thinking',
@@ -6801,7 +6783,7 @@ class OpenAiExecutionTools extends OpenAiCompatibleExecutionTools {
6801
6783
  *
6802
6784
  * This is useful for calling OpenAI API with a single assistant, for more wide usage use `OpenAiExecutionTools`.
6803
6785
  *
6804
- * Note: [🦖] There are several different things in Promptbook:
6786
+ * !!! Note: [🦖] There are several different things in Promptbook:
6805
6787
  * - `Agent` - which represents an AI Agent with its source, memories, actions, etc. Agent is a higher-level abstraction which is internally using:
6806
6788
  * - `LlmExecutionTools` - which wraps one or more LLM models and provides an interface to execute them
6807
6789
  * - `AgentLlmExecutionTools` - which is a specific implementation of `LlmExecutionTools` that wraps another LlmExecutionTools and applies agent-specific system prompts and requirements
@@ -6907,17 +6889,21 @@ class OpenAiAssistantExecutionTools extends OpenAiExecutionTools {
6907
6889
  console.info('connect', stream.currentEvent);
6908
6890
  }
6909
6891
  });
6892
+ /*
6910
6893
  stream.on('messageDelta', (messageDelta) => {
6911
- var _a;
6912
- if (this.options.isVerbose &&
6894
+ if (
6895
+ this.options.isVerbose &&
6913
6896
  messageDelta &&
6914
6897
  messageDelta.content &&
6915
6898
  messageDelta.content[0] &&
6916
- messageDelta.content[0].type === 'text') {
6917
- console.info('messageDelta', (_a = messageDelta.content[0].text) === null || _a === void 0 ? void 0 : _a.value);
6899
+ messageDelta.content[0].type === 'text'
6900
+ ) {
6901
+ console.info('messageDelta', messageDelta.content[0].text?.value);
6918
6902
  }
6903
+
6919
6904
  // <- TODO: [🐚] Make streaming and running tasks working
6920
6905
  });
6906
+ */
6921
6907
  stream.on('messageCreated', (message) => {
6922
6908
  if (this.options.isVerbose) {
6923
6909
  console.info('messageCreated', message);
@@ -6972,15 +6958,19 @@ class OpenAiAssistantExecutionTools extends OpenAiExecutionTools {
6972
6958
  },
6973
6959
  });
6974
6960
  }
6975
- async playground() {
6961
+ /*
6962
+ public async playground() {
6976
6963
  const client = await this.getClient();
6964
+
6977
6965
  // List all assistants
6978
6966
  const assistants = await client.beta.assistants.list();
6979
6967
  console.log('!!! Assistants:', assistants);
6968
+
6980
6969
  // Get details of a specific assistant
6981
6970
  const assistantId = 'asst_MO8fhZf4dGloCfXSHeLcIik0';
6982
6971
  const assistant = await client.beta.assistants.retrieve(assistantId);
6983
6972
  console.log('!!! Assistant Details:', assistant);
6973
+
6984
6974
  // Update an assistant
6985
6975
  const updatedAssistant = await client.beta.assistants.update(assistantId, {
6986
6976
  name: assistant.name + '(M)',
@@ -6990,71 +6980,103 @@ class OpenAiAssistantExecutionTools extends OpenAiExecutionTools {
6990
6980
  },
6991
6981
  });
6992
6982
  console.log('!!! Updated Assistant:', updatedAssistant);
6983
+
6993
6984
  await forEver();
6994
6985
  }
6986
+ */
6987
+ /**
6988
+ * Get an existing assistant tool wrapper
6989
+ */
6990
+ getAssistant(assistantId) {
6991
+ return new OpenAiAssistantExecutionTools({
6992
+ ...this.options,
6993
+ assistantId,
6994
+ });
6995
+ }
6995
6996
  async createNewAssistant(options) {
6996
6997
  if (!this.isCreatingNewAssistantsAllowed) {
6997
6998
  throw new NotAllowed(`Creating new assistants is not allowed. Set \`isCreatingNewAssistantsAllowed: true\` in options to enable this feature.`);
6998
6999
  }
6999
7000
  // await this.playground();
7000
- const { name, instructions } = options;
7001
+ const { name, instructions, knowledgeSources } = options;
7001
7002
  const client = await this.getClient();
7002
- /*/
7003
- //TODO: !!!
7004
- async function downloadFile(url: string, folder = './tmp'): Promise<string> {
7005
- const filename = path.basename(url.split('?')[0]);
7006
- const filepath = path.join(folder, filename);
7007
-
7008
- if (!fs.existsSync(folder)) fs.mkdirSync(folder);
7009
-
7010
- const res = await fetch(url);
7011
- if (!res.ok) throw new Error(`Download error: ${url}`);
7012
- const buffer = await res.arrayBuffer();
7013
- fs.writeFileSync(filepath, Buffer.from(buffer));
7014
- console.log(`📥 File downloaded: ${filename}`);
7015
-
7016
- return filepath;
7017
- }
7018
-
7019
- async function uploadFileToOpenAI(filepath: string) {
7020
- const file = await client.files.create({
7021
- file: fs.createReadStream(filepath),
7022
- purpose: 'assistants',
7003
+ let vectorStoreId;
7004
+ // If knowledge sources are provided, create a vector store with them
7005
+ if (knowledgeSources && knowledgeSources.length > 0) {
7006
+ if (this.options.isVerbose) {
7007
+ console.info(`📚 Creating vector store with ${knowledgeSources.length} knowledge sources...`);
7008
+ }
7009
+ // Create a vector store
7010
+ const vectorStore = await client.beta.vectorStores.create({
7011
+ name: `${name} Knowledge Base`,
7023
7012
  });
7024
- console.log(`⬆️ File uploaded to OpenAI: ${file.filename} (${file.id})`);
7025
- return file;
7026
- }
7027
-
7028
- // 🌐 URL addresses of files to upload
7029
- const fileUrls = [
7030
- 'https://raw.githubusercontent.com/vercel/next.js/canary/packages/next/README.md',
7031
- 'https://raw.githubusercontent.com/openai/openai-cookbook/main/examples/How_to_call_the_Assistants_API_with_Node.js.ipynb',
7032
- ];
7033
-
7034
- // 1️⃣ Download files from URL
7035
- const localFiles = [];
7036
- for (const url of fileUrls) {
7037
- const filepath = await downloadFile(url);
7038
- localFiles.push(filepath);
7039
- }
7040
-
7041
- // 2️⃣ Upload files to OpenAI
7042
- const uploadedFiles = [];
7043
- for (const filepath of localFiles) {
7044
- const file = await uploadFileToOpenAI(filepath);
7045
- uploadedFiles.push(file.id);
7013
+ vectorStoreId = vectorStore.id;
7014
+ if (this.options.isVerbose) {
7015
+ console.info(`✅ Vector store created: ${vectorStoreId}`);
7016
+ }
7017
+ // Upload files from knowledge sources to the vector store
7018
+ const fileStreams = [];
7019
+ for (const source of knowledgeSources) {
7020
+ try {
7021
+ // Check if it's a URL
7022
+ if (source.startsWith('http://') || source.startsWith('https://')) {
7023
+ // Download the file
7024
+ const response = await fetch(source);
7025
+ if (!response.ok) {
7026
+ console.error(`Failed to download ${source}: ${response.statusText}`);
7027
+ continue;
7028
+ }
7029
+ const buffer = await response.arrayBuffer();
7030
+ const filename = source.split('/').pop() || 'downloaded-file';
7031
+ const blob = new Blob([buffer]);
7032
+ const file = new File([blob], filename);
7033
+ fileStreams.push(file);
7034
+ }
7035
+ else {
7036
+ // Assume it's a local file path
7037
+ // Note: This will work in Node.js environment
7038
+ // For browser environments, this would need different handling
7039
+ const fs = await import('fs');
7040
+ const fileStream = fs.createReadStream(source);
7041
+ fileStreams.push(fileStream);
7042
+ }
7043
+ }
7044
+ catch (error) {
7045
+ console.error(`Error processing knowledge source ${source}:`, error);
7046
+ }
7047
+ }
7048
+ // Batch upload files to the vector store
7049
+ if (fileStreams.length > 0) {
7050
+ try {
7051
+ await client.beta.vectorStores.fileBatches.uploadAndPoll(vectorStoreId, {
7052
+ files: fileStreams,
7053
+ });
7054
+ if (this.options.isVerbose) {
7055
+ console.info(`✅ Uploaded ${fileStreams.length} files to vector store`);
7056
+ }
7057
+ }
7058
+ catch (error) {
7059
+ console.error('Error uploading files to vector store:', error);
7060
+ }
7061
+ }
7046
7062
  }
7047
- /**/
7048
- // alert('!!!! Creating new OpenAI assistant');
7049
- // 3️⃣ Create assistant with uploaded files
7050
- const assistant = await client.beta.assistants.create({
7063
+ // Create assistant with vector store attached
7064
+ const assistantConfig = {
7051
7065
  name,
7052
7066
  description: 'Assistant created via Promptbook',
7053
7067
  model: 'gpt-4o',
7054
7068
  instructions,
7055
7069
  tools: [/* TODO: [🧠] Maybe add { type: 'code_interpreter' }, */ { type: 'file_search' }],
7056
- // !!!! file_ids: uploadedFiles,
7057
- });
7070
+ };
7071
+ // Attach vector store if created
7072
+ if (vectorStoreId) {
7073
+ assistantConfig.tool_resources = {
7074
+ file_search: {
7075
+ vector_store_ids: [vectorStoreId],
7076
+ },
7077
+ };
7078
+ }
7079
+ const assistant = await client.beta.assistants.create(assistantConfig);
7058
7080
  console.log(`✅ Assistant created: ${assistant.id}`);
7059
7081
  // TODO: !!!! Try listing existing assistants
7060
7082
  // TODO: !!!! Try marking existing assistants by DISCRIMINANT
@@ -7065,6 +7087,95 @@ class OpenAiAssistantExecutionTools extends OpenAiExecutionTools {
7065
7087
  assistantId: assistant.id,
7066
7088
  });
7067
7089
  }
7090
+ async updateAssistant(options) {
7091
+ if (!this.isCreatingNewAssistantsAllowed) {
7092
+ throw new NotAllowed(`Updating assistants is not allowed. Set \`isCreatingNewAssistantsAllowed: true\` in options to enable this feature.`);
7093
+ }
7094
+ const { assistantId, name, instructions, knowledgeSources } = options;
7095
+ const client = await this.getClient();
7096
+ let vectorStoreId;
7097
+ // If knowledge sources are provided, create a vector store with them
7098
+ // TODO: [🧠] Reuse vector store creation logic from createNewAssistant
7099
+ if (knowledgeSources && knowledgeSources.length > 0) {
7100
+ if (this.options.isVerbose) {
7101
+ console.info(`📚 Creating vector store for update with ${knowledgeSources.length} knowledge sources...`);
7102
+ }
7103
+ // Create a vector store
7104
+ const vectorStore = await client.beta.vectorStores.create({
7105
+ name: `${name} Knowledge Base`,
7106
+ });
7107
+ vectorStoreId = vectorStore.id;
7108
+ if (this.options.isVerbose) {
7109
+ console.info(`✅ Vector store created: ${vectorStoreId}`);
7110
+ }
7111
+ // Upload files from knowledge sources to the vector store
7112
+ const fileStreams = [];
7113
+ for (const source of knowledgeSources) {
7114
+ try {
7115
+ // Check if it's a URL
7116
+ if (source.startsWith('http://') || source.startsWith('https://')) {
7117
+ // Download the file
7118
+ const response = await fetch(source);
7119
+ if (!response.ok) {
7120
+ console.error(`Failed to download ${source}: ${response.statusText}`);
7121
+ continue;
7122
+ }
7123
+ const buffer = await response.arrayBuffer();
7124
+ const filename = source.split('/').pop() || 'downloaded-file';
7125
+ const blob = new Blob([buffer]);
7126
+ const file = new File([blob], filename);
7127
+ fileStreams.push(file);
7128
+ }
7129
+ else {
7130
+ // Assume it's a local file path
7131
+ // Note: This will work in Node.js environment
7132
+ // For browser environments, this would need different handling
7133
+ const fs = await import('fs');
7134
+ const fileStream = fs.createReadStream(source);
7135
+ fileStreams.push(fileStream);
7136
+ }
7137
+ }
7138
+ catch (error) {
7139
+ console.error(`Error processing knowledge source ${source}:`, error);
7140
+ }
7141
+ }
7142
+ // Batch upload files to the vector store
7143
+ if (fileStreams.length > 0) {
7144
+ try {
7145
+ await client.beta.vectorStores.fileBatches.uploadAndPoll(vectorStoreId, {
7146
+ files: fileStreams,
7147
+ });
7148
+ if (this.options.isVerbose) {
7149
+ console.info(`✅ Uploaded ${fileStreams.length} files to vector store`);
7150
+ }
7151
+ }
7152
+ catch (error) {
7153
+ console.error('Error uploading files to vector store:', error);
7154
+ }
7155
+ }
7156
+ }
7157
+ const assistantUpdate = {
7158
+ name,
7159
+ instructions,
7160
+ tools: [/* TODO: [🧠] Maybe add { type: 'code_interpreter' }, */ { type: 'file_search' }],
7161
+ };
7162
+ if (vectorStoreId) {
7163
+ assistantUpdate.tool_resources = {
7164
+ file_search: {
7165
+ vector_store_ids: [vectorStoreId],
7166
+ },
7167
+ };
7168
+ }
7169
+ const assistant = await client.beta.assistants.update(assistantId, assistantUpdate);
7170
+ if (this.options.isVerbose) {
7171
+ console.log(`✅ Assistant updated: ${assistant.id}`);
7172
+ }
7173
+ return new OpenAiAssistantExecutionTools({
7174
+ ...this.options,
7175
+ isCreatingNewAssistantsAllowed: false,
7176
+ assistantId: assistant.id,
7177
+ });
7178
+ }
7068
7179
  /**
7069
7180
  * Discriminant for type guards
7070
7181
  */
@@ -7522,6 +7633,8 @@ function nameToSubfolderPath(name) {
7522
7633
  /**
7523
7634
  * Converts a given text to kebab-case format.
7524
7635
  *
7636
+ * Note: [🔂] This function is idempotent.
7637
+ *
7525
7638
  * @param text The text to be converted.
7526
7639
  * @returns The kebab-case formatted string.
7527
7640
  * @example 'hello-world'
@@ -7647,6 +7760,8 @@ function removeEmojis(text) {
7647
7760
  /**
7648
7761
  * Converts a title string into a normalized name.
7649
7762
  *
7763
+ * Note: [🔂] This function is idempotent.
7764
+ *
7650
7765
  * @param value The title string to be converted to a name.
7651
7766
  * @returns A normalized name derived from the input title.
7652
7767
  * @example 'Hello World!' -> 'hello-world'
@@ -13266,40 +13381,6 @@ const _FormattedBookInMarkdownTranspilerRegistration = $bookTranspilersRegister.
13266
13381
  * Note: [💞] Ignore a discrepancy between file name and entity name
13267
13382
  */
13268
13383
 
13269
- /**
13270
- * Creates an empty/basic agent model requirements object
13271
- * This serves as the starting point for the reduce-like pattern
13272
- * where each commitment applies its changes to build the final requirements
13273
- *
13274
- * @public exported from `@promptbook/core`
13275
- */
13276
- function createEmptyAgentModelRequirements() {
13277
- return {
13278
- systemMessage: '',
13279
- // modelName: 'gpt-5',
13280
- modelName: 'gemini-2.5-flash-lite',
13281
- temperature: 0.7,
13282
- topP: 0.9,
13283
- topK: 50,
13284
- };
13285
- }
13286
- /**
13287
- * Creates a basic agent model requirements with just the agent name
13288
- * This is used when we have an agent name but no commitments
13289
- *
13290
- * @public exported from `@promptbook/core`
13291
- */
13292
- function createBasicAgentModelRequirements(agentName) {
13293
- const empty = createEmptyAgentModelRequirements();
13294
- return {
13295
- ...empty,
13296
- systemMessage: `You are ${agentName || 'AI Agent'}`,
13297
- };
13298
- }
13299
- /**
13300
- * TODO: [🐤] Deduplicate `AgentModelRequirements` and `ModelRequirements` model requirements
13301
- */
13302
-
13303
13384
  /**
13304
13385
  * Generates a regex pattern to match a specific commitment
13305
13386
  *
@@ -13833,23 +13914,19 @@ class KnowledgeCommitmentDefinition extends BaseCommitmentDefinition {
13833
13914
  `);
13834
13915
  }
13835
13916
  applyToAgentModelRequirements(requirements, content) {
13836
- var _a;
13837
13917
  const trimmedContent = content.trim();
13838
13918
  if (!trimmedContent) {
13839
13919
  return requirements;
13840
13920
  }
13841
13921
  // Check if content is a URL (external knowledge source)
13842
- if (this.isUrl(trimmedContent)) {
13922
+ if (isValidUrl(trimmedContent)) {
13843
13923
  // Store the URL for later async processing
13844
13924
  const updatedRequirements = {
13845
13925
  ...requirements,
13846
- metadata: {
13847
- ...requirements.metadata,
13848
- knowledgeSources: [
13849
- ...(((_a = requirements.metadata) === null || _a === void 0 ? void 0 : _a.knowledgeSources) || []),
13850
- trimmedContent,
13851
- ],
13852
- },
13926
+ knowledgeSources: [
13927
+ ...(requirements.knowledgeSources || []),
13928
+ trimmedContent,
13929
+ ],
13853
13930
  };
13854
13931
  // Add placeholder information about knowledge sources to system message
13855
13932
  const knowledgeInfo = `Knowledge Source URL: ${trimmedContent} (will be processed for retrieval during chat)`;
@@ -13861,18 +13938,6 @@ class KnowledgeCommitmentDefinition extends BaseCommitmentDefinition {
13861
13938
  return this.appendToSystemMessage(requirements, knowledgeSection, '\n\n');
13862
13939
  }
13863
13940
  }
13864
- /**
13865
- * Check if content is a URL
13866
- */
13867
- isUrl(content) {
13868
- try {
13869
- new URL(content);
13870
- return true;
13871
- }
13872
- catch (_a) {
13873
- return false;
13874
- }
13875
- }
13876
13941
  }
13877
13942
  /**
13878
13943
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -14683,6 +14748,7 @@ class PersonaCommitmentDefinition extends BaseCommitmentDefinition {
14683
14748
  // Keep everything after the PERSONA section
14684
14749
  cleanedMessage = lines.slice(personaEndIndex).join('\n').trim();
14685
14750
  }
14751
+ // TODO: [🕛] There should be `agentFullname` not `agentName`
14686
14752
  // Create new system message with persona at the beginning
14687
14753
  // Format: "You are {agentName}\n{personaContent}"
14688
14754
  // The # PERSONA comment will be removed later by removeCommentsFromSystemMessage
@@ -15170,6 +15236,40 @@ function getCommitmentDefinition(type) {
15170
15236
  * Note: [💞] Ignore a discrepancy between file name and entity name
15171
15237
  */
15172
15238
 
15239
+ /**
15240
+ * Creates an empty/basic agent model requirements object
15241
+ * This serves as the starting point for the reduce-like pattern
15242
+ * where each commitment applies its changes to build the final requirements
15243
+ *
15244
+ * @public exported from `@promptbook/core`
15245
+ */
15246
+ function createEmptyAgentModelRequirements() {
15247
+ return {
15248
+ systemMessage: '',
15249
+ // modelName: 'gpt-5',
15250
+ modelName: 'gemini-2.5-flash-lite',
15251
+ temperature: 0.7,
15252
+ topP: 0.9,
15253
+ topK: 50,
15254
+ };
15255
+ }
15256
+ /**
15257
+ * Creates a basic agent model requirements with just the agent name
15258
+ * This is used when we have an agent name but no commitments
15259
+ *
15260
+ * @public exported from `@promptbook/core`
15261
+ */
15262
+ function createBasicAgentModelRequirements(agentName) {
15263
+ const empty = createEmptyAgentModelRequirements();
15264
+ return {
15265
+ ...empty,
15266
+ systemMessage: `You are ${agentName || 'AI Agent'}`,
15267
+ };
15268
+ }
15269
+ /**
15270
+ * TODO: [🐤] Deduplicate `AgentModelRequirements` and `ModelRequirements` model requirements
15271
+ */
15272
+
15173
15273
  /**
15174
15274
  * Parses agent source using the new commitment system with multiline support
15175
15275
  * This function replaces the hardcoded commitment parsing in the original parseAgentSource
@@ -15260,29 +15360,6 @@ function parseAgentSourceWithCommitments(agentSource) {
15260
15360
  };
15261
15361
  }
15262
15362
 
15263
- /**
15264
- * Removes comment lines (lines starting with #) from a system message
15265
- * This is used to clean up the final system message before sending it to the AI model
15266
- * while preserving the original content with comments in metadata
15267
- *
15268
- * @param systemMessage The system message that may contain comment lines
15269
- * @returns The system message with comment lines removed
15270
- *
15271
- * @private - TODO: [🧠] Maybe should be public?
15272
- */
15273
- function removeCommentsFromSystemMessage(systemMessage) {
15274
- if (!systemMessage) {
15275
- return systemMessage;
15276
- }
15277
- const lines = systemMessage.split('\n');
15278
- const filteredLines = lines.filter((line) => {
15279
- const trimmedLine = line.trim();
15280
- // Remove lines that start with # (comments)
15281
- return !trimmedLine.startsWith('#');
15282
- });
15283
- return filteredLines.join('\n').trim();
15284
- }
15285
-
15286
15363
  /**
15287
15364
  * Parses parameters from text using both supported notations:
15288
15365
  * 1. @Parameter - single word parameter starting with @
@@ -15342,17 +15419,40 @@ function parseParameters(text) {
15342
15419
  }
15343
15420
 
15344
15421
  /**
15345
- * Creates agent model requirements using the new commitment system
15346
- * This function uses a reduce-like pattern where each commitment applies its changes
15347
- * to build the final requirements starting from a basic empty model
15422
+ * Removes comment lines (lines starting with #) from a system message
15423
+ * This is used to clean up the final system message before sending it to the AI model
15424
+ * while preserving the original content with comments in metadata
15348
15425
  *
15349
- * @public exported from `@promptbook/core`
15426
+ * @param systemMessage The system message that may contain comment lines
15427
+ * @returns The system message with comment lines removed
15428
+ *
15429
+ * @private - TODO: [🧠] Maybe should be public?
15350
15430
  */
15351
- async function createAgentModelRequirementsWithCommitments(agentSource, modelName) {
15352
- // Parse the agent source to extract commitments
15353
- const parseResult = parseAgentSourceWithCommitments(agentSource);
15354
- // Apply DELETE filtering: remove prior commitments tagged by parameters targeted by DELETE/CANCEL/DISCARD/REMOVE
15355
- const filteredCommitments = [];
15431
+ function removeCommentsFromSystemMessage(systemMessage) {
15432
+ if (!systemMessage) {
15433
+ return systemMessage;
15434
+ }
15435
+ const lines = systemMessage.split('\n');
15436
+ const filteredLines = lines.filter((line) => {
15437
+ const trimmedLine = line.trim();
15438
+ // Remove lines that start with # (comments)
15439
+ return !trimmedLine.startsWith('#');
15440
+ });
15441
+ return filteredLines.join('\n').trim();
15442
+ }
15443
+
15444
+ /**
15445
+ * Creates agent model requirements using the new commitment system
15446
+ * This function uses a reduce-like pattern where each commitment applies its changes
15447
+ * to build the final requirements starting from a basic empty model
15448
+ *
15449
+ * @public exported from `@promptbook/core`
15450
+ */
15451
+ async function createAgentModelRequirementsWithCommitments(agentSource, modelName) {
15452
+ // Parse the agent source to extract commitments
15453
+ const parseResult = parseAgentSourceWithCommitments(agentSource);
15454
+ // Apply DELETE filtering: remove prior commitments tagged by parameters targeted by DELETE/CANCEL/DISCARD/REMOVE
15455
+ const filteredCommitments = [];
15356
15456
  for (const commitment of parseResult.commitments) {
15357
15457
  // Handle DELETE-like commitments by invalidating prior tagged commitments
15358
15458
  if (commitment.type === 'DELETE' ||
@@ -15442,6 +15542,8 @@ async function createAgentModelRequirementsWithCommitments(agentSource, modelNam
15442
15542
  /**
15443
15543
  * Normalizes a given text to camelCase format.
15444
15544
  *
15545
+ * Note: [🔂] This function is idempotent.
15546
+ *
15445
15547
  * @param text The text to be normalized.
15446
15548
  * @param _isFirstLetterCapital Whether the first letter should be capitalized.
15447
15549
  * @returns The camelCase formatted string.
@@ -15530,154 +15632,501 @@ function generatePlaceholderAgentProfileImageUrl(agentName) {
15530
15632
  */
15531
15633
 
15532
15634
  /**
15533
- * Parses basic information from agent source
15534
- *
15535
- * There are 2 similar functions:
15536
- * - `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.
15537
- * - `createAgentModelRequirements` which is an asynchronous function that creates model requirements it applies each commitment one by one and works asynchronously.
15635
+ * Computes SHA-256 hash of the agent source
15538
15636
  *
15539
15637
  * @public exported from `@promptbook/core`
15540
15638
  */
15541
- function parseAgentSource(agentSource) {
15542
- const parseResult = parseAgentSourceWithCommitments(agentSource);
15543
- // Find PERSONA and META commitments
15544
- let personaDescription = null;
15545
- for (const commitment of parseResult.commitments) {
15546
- if (commitment.type !== 'PERSONA') {
15547
- continue;
15548
- }
15549
- if (personaDescription === null) {
15550
- personaDescription = '';
15551
- }
15552
- else {
15553
- personaDescription += `\n\n${personaDescription}`;
15639
+ function computeAgentHash(agentSource) {
15640
+ return SHA256(hexEncoder.parse(agentSource /* <- TODO: !!!!! spaceTrim */)).toString( /* hex */);
15641
+ }
15642
+
15643
+ /**
15644
+ * Function parseNumber will parse number from string
15645
+ *
15646
+ * Note: [🔂] This function is idempotent.
15647
+ * Unlike Number.parseInt, Number.parseFloat it will never ever result in NaN
15648
+ * Note: it also works only with decimal numbers
15649
+ *
15650
+ * @returns parsed number
15651
+ * @throws {ParseError} if the value is not a number
15652
+ *
15653
+ * @public exported from `@promptbook/utils`
15654
+ */
15655
+ function parseNumber(value) {
15656
+ const originalValue = value;
15657
+ if (typeof value === 'number') {
15658
+ value = value.toString(); // <- TODO: Maybe more efficient way to do this
15659
+ }
15660
+ if (typeof value !== 'string') {
15661
+ return 0;
15662
+ }
15663
+ value = value.trim();
15664
+ if (value.startsWith('+')) {
15665
+ return parseNumber(value.substring(1));
15666
+ }
15667
+ if (value.startsWith('-')) {
15668
+ const number = parseNumber(value.substring(1));
15669
+ if (number === 0) {
15670
+ return 0; // <- Note: To prevent -0
15554
15671
  }
15555
- personaDescription += commitment.content;
15672
+ return -number;
15556
15673
  }
15557
- const meta = {};
15558
- for (const commitment of parseResult.commitments) {
15559
- if (commitment.type !== 'META') {
15560
- continue;
15674
+ value = value.replace(/,/g, '.');
15675
+ value = value.toUpperCase();
15676
+ if (value === '') {
15677
+ return 0;
15678
+ }
15679
+ if (value === '♾' || value.startsWith('INF')) {
15680
+ return Infinity;
15681
+ }
15682
+ if (value.includes('/')) {
15683
+ const [numerator_, denominator_] = value.split('/');
15684
+ const numerator = parseNumber(numerator_);
15685
+ const denominator = parseNumber(denominator_);
15686
+ if (denominator === 0) {
15687
+ throw new ParseError(`Unable to parse number from "${originalValue}" because denominator is zero`);
15561
15688
  }
15562
- // Parse META commitments - format is "META TYPE content"
15563
- const metaTypeRaw = commitment.content.split(' ')[0] || 'NONE';
15564
- const metaType = normalizeTo_camelCase(metaTypeRaw);
15565
- meta[metaType] = spaceTrim(commitment.content.substring(metaTypeRaw.length));
15689
+ return numerator / denominator;
15566
15690
  }
15567
- // Generate gravatar fallback if no meta image specified
15568
- if (!meta.image) {
15569
- meta.image = generatePlaceholderAgentProfileImageUrl(parseResult.agentName || '!!');
15691
+ if (/^(NAN|NULL|NONE|UNDEFINED|ZERO|NO.*)$/.test(value)) {
15692
+ return 0;
15570
15693
  }
15571
- // Parse parameters using unified approach - both @Parameter and {parameter} notations
15572
- // are treated as the same syntax feature with unified representation
15573
- const parameters = parseParameters(agentSource);
15574
- return {
15575
- agentName: parseResult.agentName,
15576
- personaDescription,
15577
- meta,
15578
- parameters,
15579
- };
15694
+ if (value.includes('E')) {
15695
+ const [significand, exponent] = value.split('E');
15696
+ return parseNumber(significand) * 10 ** parseNumber(exponent);
15697
+ }
15698
+ if (!/^[0-9.]+$/.test(value) || value.split('.').length > 2) {
15699
+ throw new ParseError(`Unable to parse number from "${originalValue}"`);
15700
+ }
15701
+ const num = parseFloat(value);
15702
+ if (isNaN(num)) {
15703
+ throw new ParseError(`Unexpected NaN when parsing number from "${originalValue}"`);
15704
+ }
15705
+ return num;
15580
15706
  }
15581
15707
  /**
15582
- * TODO: [🕛] Unite `AgentBasicInformation`, `ChatParticipant`, `LlmExecutionTools` + `LlmToolsMetadata`
15708
+ * TODO: Maybe use sth. like safe-eval in fraction/calculation case @see https://www.npmjs.com/package/safe-eval
15709
+ * TODO: [🧠][🌻] Maybe export through `@promptbook/markdown-utils` not `@promptbook/utils`
15583
15710
  */
15584
15711
 
15585
15712
  /**
15586
- * Creates model requirements for an agent based on its source
15713
+ * Makes first letter of a string lowercase
15587
15714
  *
15588
- * There are 2 similar functions:
15589
- * - `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.
15590
- * - `createAgentModelRequirements` which is an asynchronous function that creates model requirements it applies each commitment one by one and works asynchronous.
15715
+ * Note: [🔂] This function is idempotent.
15591
15716
  *
15592
- * @public exported from `@promptbook/core`
15717
+ * @public exported from `@promptbook/utils`
15593
15718
  */
15594
- async function createAgentModelRequirements(agentSource, modelName, availableModels, llmTools) {
15595
- // If availableModels are provided and no specific modelName is given,
15596
- // use preparePersona to select the best model
15597
- if (availableModels && !modelName && llmTools) {
15598
- const selectedModelName = await selectBestModelUsingPersona(agentSource, llmTools);
15599
- return createAgentModelRequirementsWithCommitments(agentSource, selectedModelName);
15600
- }
15601
- // Use the new commitment-based system with provided or default model
15602
- return createAgentModelRequirementsWithCommitments(agentSource, modelName);
15719
+ function decapitalize(word) {
15720
+ return word.substring(0, 1).toLowerCase() + word.substring(1);
15603
15721
  }
15722
+
15604
15723
  /**
15605
- * Selects the best model using the preparePersona function
15606
- * This directly uses preparePersona to ensure DRY principle
15724
+ * Parses keywords from a string
15607
15725
  *
15608
- * @param agentSource The agent source to derive persona description from
15609
- * @param llmTools LLM tools for preparing persona
15610
- * @returns The name of the best selected model
15611
- * @private function of `createAgentModelRequirements`
15726
+ * @param {string} input
15727
+ * @returns {Set} of keywords without diacritics in lowercase
15728
+ * @public exported from `@promptbook/utils`
15612
15729
  */
15613
- async function selectBestModelUsingPersona(agentSource, llmTools) {
15614
- var _a;
15615
- // Parse agent source to get persona description
15616
- const { agentName, personaDescription } = parseAgentSource(agentSource);
15617
- // Use agent name as fallback if no persona description is available
15618
- const description = personaDescription || agentName || 'AI Agent';
15619
- try {
15620
- // Use preparePersona directly
15621
- const { modelsRequirements } = await preparePersona(description, { llm: llmTools }, { isVerbose: false });
15622
- // Extract the first model name from the requirements
15623
- if (modelsRequirements.length > 0 && ((_a = modelsRequirements[0]) === null || _a === void 0 ? void 0 : _a.modelName)) {
15624
- return modelsRequirements[0].modelName;
15625
- }
15626
- // Fallback: get available models and return the first CHAT model
15627
- const availableModels = await llmTools.listModels();
15628
- const chatModels = availableModels.filter(({ modelVariant }) => modelVariant === 'CHAT');
15629
- if (chatModels.length === 0) {
15630
- throw new Error('No CHAT models available for agent model selection');
15631
- }
15632
- return chatModels[0].modelName;
15633
- }
15634
- catch (error) {
15635
- console.warn('Failed to use preparePersona for model selection, falling back to first available model:', error);
15636
- // Fallback: get available models and return the first CHAT model
15637
- const availableModels = await llmTools.listModels();
15638
- const chatModels = availableModels.filter(({ modelVariant }) => modelVariant === 'CHAT');
15639
- if (chatModels.length === 0) {
15640
- throw new Error('No CHAT models available for agent model selection');
15641
- }
15642
- return chatModels[0].modelName;
15643
- }
15730
+ function parseKeywordsFromString(input) {
15731
+ const keywords = normalizeTo_SCREAMING_CASE(removeDiacritics(input))
15732
+ .toLowerCase()
15733
+ .split(/[^a-z0-9]+/gs)
15734
+ .filter((value) => value);
15735
+ return new Set(keywords);
15644
15736
  }
15737
+
15645
15738
  /**
15646
- * Extracts MCP servers from agent source
15739
+ * Converts a name string into a URI-compatible format.
15647
15740
  *
15648
- * @param agentSource The agent source string that may contain MCP lines
15649
- * @returns Array of MCP server identifiers
15741
+ * @param name The string to be converted to a URI-compatible format.
15742
+ * @returns A URI-compatible string derived from the input name.
15743
+ * @example 'Hello World' -> 'hello-world'
15744
+ * @public exported from `@promptbook/utils`
15745
+ */
15746
+ function nameToUriPart(name) {
15747
+ let uriPart = name;
15748
+ uriPart = uriPart.toLowerCase();
15749
+ uriPart = removeDiacritics(uriPart);
15750
+ uriPart = uriPart.replace(/[^a-zA-Z0-9]+/g, '-');
15751
+ uriPart = uriPart.replace(/^-+/, '');
15752
+ uriPart = uriPart.replace(/-+$/, '');
15753
+ return uriPart;
15754
+ }
15755
+
15756
+ /**
15757
+ * Converts a given name into URI-compatible parts.
15650
15758
  *
15651
- * @private TODO: [🧠] Maybe should be public
15759
+ * @param name The name to be converted into URI parts.
15760
+ * @returns An array of URI-compatible parts derived from the name.
15761
+ * @example 'Example Name' -> ['example', 'name']
15762
+ * @public exported from `@promptbook/utils`
15652
15763
  */
15653
- function extractMcpServers(agentSource) {
15654
- if (!agentSource) {
15655
- return [];
15656
- }
15657
- const lines = agentSource.split('\n');
15658
- const mcpRegex = /^\s*MCP\s+(.+)$/i;
15659
- const mcpServers = [];
15660
- // Look for MCP lines
15661
- for (const line of lines) {
15662
- const match = line.match(mcpRegex);
15663
- if (match && match[1]) {
15664
- mcpServers.push(match[1].trim());
15665
- }
15666
- }
15667
- return mcpServers;
15764
+ function nameToUriParts(name) {
15765
+ return nameToUriPart(name)
15766
+ .split('-')
15767
+ .filter((value) => value !== '');
15668
15768
  }
15669
15769
 
15670
15770
  /**
15671
- * Transpiler to Javascript code using OpenAI SDK.
15771
+ * Normalizes a given text to PascalCase format.
15672
15772
  *
15673
- * @public exported from `@promptbook/core`
15773
+ * Note: [🔂] This function is idempotent.
15774
+ *
15775
+ * @param text @public exported from `@promptbook/utils`
15776
+ * @returns
15777
+ * @example 'HelloWorld'
15778
+ * @example 'ILovePromptbook'
15779
+ * @public exported from `@promptbook/utils`
15674
15780
  */
15675
- const OpenAiSdkTranspiler = {
15676
- name: 'openai-sdk',
15677
- title: 'OpenAI SDK',
15678
- packageName: '@promptbook/core',
15679
- className: 'OpenAiSdkTranspiler',
15680
- async transpileBook(book, tools, options) {
15781
+ function normalizeTo_PascalCase(text) {
15782
+ return normalizeTo_camelCase(text, true);
15783
+ }
15784
+
15785
+ /**
15786
+ * Take every whitespace (space, new line, tab) and replace it with a single space
15787
+ *
15788
+ * Note: [🔂] This function is idempotent.
15789
+ *
15790
+ * @public exported from `@promptbook/utils`
15791
+ */
15792
+ function normalizeWhitespaces(sentence) {
15793
+ return sentence.replace(/\s+/gs, ' ').trim();
15794
+ }
15795
+
15796
+ /**
15797
+ * Removes quotes from a string
15798
+ *
15799
+ * Note: [🔂] This function is idempotent.
15800
+ * Tip: This is very useful for post-processing of the result of the LLM model
15801
+ * Note: This function removes only the same quotes from the beginning and the end of the string
15802
+ * Note: There are two similar functions:
15803
+ * - `removeQuotes` which removes only bounding quotes
15804
+ * - `unwrapResult` which removes whole introduce sentence
15805
+ *
15806
+ * @param text optionally quoted text
15807
+ * @returns text without quotes
15808
+ * @public exported from `@promptbook/utils`
15809
+ */
15810
+ function removeQuotes(text) {
15811
+ if (text.startsWith('"') && text.endsWith('"')) {
15812
+ return text.slice(1, -1);
15813
+ }
15814
+ if (text.startsWith("'") && text.endsWith("'")) {
15815
+ return text.slice(1, -1);
15816
+ }
15817
+ return text;
15818
+ }
15819
+
15820
+ /**
15821
+ * Removes quotes and optional introduce text from a string
15822
+ *
15823
+ * Tip: This is very useful for post-processing of the result of the LLM model
15824
+ * Note: This function trims the text and removes whole introduce sentence if it is present
15825
+ * Note: There are two similar functions:
15826
+ * - `removeQuotes` which removes only bounding quotes
15827
+ * - `unwrapResult` which removes whole introduce sentence
15828
+ *
15829
+ * @param text optionally quoted text
15830
+ * @returns text without quotes
15831
+ * @public exported from `@promptbook/utils`
15832
+ */
15833
+ function unwrapResult(text, options) {
15834
+ const { isTrimmed = true, isIntroduceSentenceRemoved = true } = options || {};
15835
+ let trimmedText = text;
15836
+ // Remove leading and trailing spaces and newlines
15837
+ if (isTrimmed) {
15838
+ trimmedText = spaceTrim$1(trimmedText);
15839
+ }
15840
+ let processedText = trimmedText;
15841
+ if (isIntroduceSentenceRemoved) {
15842
+ const introduceSentenceRegex = /^[a-zěščřžýáíéúů:\s]*:\s*/i;
15843
+ if (introduceSentenceRegex.test(text)) {
15844
+ // Remove the introduce sentence and quotes by replacing it with an empty string
15845
+ processedText = processedText.replace(introduceSentenceRegex, '');
15846
+ }
15847
+ processedText = spaceTrim$1(processedText);
15848
+ }
15849
+ if (processedText.length < 3) {
15850
+ return trimmedText;
15851
+ }
15852
+ if (processedText.includes('\n')) {
15853
+ return trimmedText;
15854
+ }
15855
+ // Remove the quotes by extracting the substring without the first and last characters
15856
+ const unquotedText = processedText.slice(1, -1);
15857
+ // Check if the text starts and ends with quotes
15858
+ if ([
15859
+ ['"', '"'],
15860
+ ["'", "'"],
15861
+ ['`', '`'],
15862
+ ['*', '*'],
15863
+ ['_', '_'],
15864
+ ['„', '“'],
15865
+ ['«', '»'] /* <- QUOTES to config */,
15866
+ ].some(([startQuote, endQuote]) => {
15867
+ if (!processedText.startsWith(startQuote)) {
15868
+ return false;
15869
+ }
15870
+ if (!processedText.endsWith(endQuote)) {
15871
+ return false;
15872
+ }
15873
+ if (unquotedText.includes(startQuote) && !unquotedText.includes(endQuote)) {
15874
+ return false;
15875
+ }
15876
+ if (!unquotedText.includes(startQuote) && unquotedText.includes(endQuote)) {
15877
+ return false;
15878
+ }
15879
+ return true;
15880
+ })) {
15881
+ return unwrapResult(unquotedText, { isTrimmed: false, isIntroduceSentenceRemoved: false });
15882
+ }
15883
+ else {
15884
+ return processedText;
15885
+ }
15886
+ }
15887
+ /**
15888
+ * TODO: [🧠] Should this also unwrap the (parenthesis)
15889
+ */
15890
+
15891
+ // <- TODO: Auto convert to type `import { ... } from 'type-fest';`
15892
+ /**
15893
+ * Tests if the value is [🚉] serializable as JSON
15894
+ *
15895
+ * - Almost all primitives are serializable BUT:
15896
+ * - `undefined` is not serializable
15897
+ * - `NaN` is not serializable
15898
+ * - Objects and arrays are serializable if all their properties are serializable
15899
+ * - Functions are not serializable
15900
+ * - Circular references are not serializable
15901
+ * - `Date` objects are not serializable
15902
+ * - `Map` and `Set` objects are not serializable
15903
+ * - `RegExp` objects are not serializable
15904
+ * - `Error` objects are not serializable
15905
+ * - `Symbol` objects are not serializable
15906
+ * - And much more...
15907
+ *
15908
+ *
15909
+ * @public exported from `@promptbook/utils`
15910
+ */
15911
+ function isSerializableAsJson(value) {
15912
+ try {
15913
+ checkSerializableAsJson({ value });
15914
+ return true;
15915
+ }
15916
+ catch (error) {
15917
+ return false;
15918
+ }
15919
+ }
15920
+ /**
15921
+ * TODO: [🧠][main] !!3 In-memory cache of same values to prevent multiple checks
15922
+ * TODO: [🧠][💺] Can be done this on type-level?
15923
+ */
15924
+
15925
+ /**
15926
+ * Determines if the given path is a root path.
15927
+ *
15928
+ * Note: This does not check if the file exists only if the path is valid
15929
+ * @public exported from `@promptbook/utils`
15930
+ */
15931
+ function isRootPath(value) {
15932
+ if (value === '/') {
15933
+ return true;
15934
+ }
15935
+ if (/^[A-Z]:\\$/i.test(value)) {
15936
+ return true;
15937
+ }
15938
+ return false;
15939
+ }
15940
+ /**
15941
+ * TODO: [🍏] Make for MacOS paths
15942
+ */
15943
+
15944
+ /**
15945
+ * Checks if the given value is a valid JavaScript identifier name.
15946
+ *
15947
+ * @param javascriptName The value to check for JavaScript identifier validity.
15948
+ * @returns `true` if the value is a valid JavaScript name, false otherwise.
15949
+ * @public exported from `@promptbook/utils`
15950
+ */
15951
+ function isValidJavascriptName(javascriptName) {
15952
+ if (typeof javascriptName !== 'string') {
15953
+ return false;
15954
+ }
15955
+ return /^[a-zA-Z_$][0-9a-zA-Z_$]*$/i.test(javascriptName);
15956
+ }
15957
+
15958
+ /**
15959
+ * Normalizes agent name from arbitrary string to valid agent name
15960
+ *
15961
+ * Note: [🔂] This function is idempotent.
15962
+ *
15963
+ * @public exported from `@promptbook/core`
15964
+ */
15965
+ function normalizeAgentName(rawAgentName) {
15966
+ return titleToName(spaceTrim(rawAgentName));
15967
+ }
15968
+
15969
+ /**
15970
+ * Creates temporary default agent name based on agent source hash
15971
+ *
15972
+ * @public exported from `@promptbook/core`
15973
+ */
15974
+ function createDefaultAgentName(agentSource) {
15975
+ const agentHash = computeAgentHash(agentSource);
15976
+ return normalizeAgentName(`Agent ${agentHash.substring(0, 6)}`);
15977
+ }
15978
+
15979
+ /**
15980
+ * Parses basic information from agent source
15981
+ *
15982
+ * There are 2 similar functions:
15983
+ * - `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.
15984
+ * - `createAgentModelRequirements` which is an asynchronous function that creates model requirements it applies each commitment one by one and works asynchronously.
15985
+ *
15986
+ * @public exported from `@promptbook/core`
15987
+ */
15988
+ function parseAgentSource(agentSource) {
15989
+ const parseResult = parseAgentSourceWithCommitments(agentSource);
15990
+ // Find PERSONA and META commitments
15991
+ let personaDescription = null;
15992
+ for (const commitment of parseResult.commitments) {
15993
+ if (commitment.type !== 'PERSONA') {
15994
+ continue;
15995
+ }
15996
+ if (personaDescription === null) {
15997
+ personaDescription = '';
15998
+ }
15999
+ else {
16000
+ personaDescription += `\n\n${personaDescription}`;
16001
+ }
16002
+ personaDescription += commitment.content;
16003
+ }
16004
+ const meta = {};
16005
+ for (const commitment of parseResult.commitments) {
16006
+ if (commitment.type !== 'META') {
16007
+ continue;
16008
+ }
16009
+ // Parse META commitments - format is "META TYPE content"
16010
+ const metaTypeRaw = commitment.content.split(' ')[0] || 'NONE';
16011
+ const metaType = normalizeTo_camelCase(metaTypeRaw);
16012
+ meta[metaType] = spaceTrim(commitment.content.substring(metaTypeRaw.length));
16013
+ }
16014
+ // Generate gravatar fallback if no meta image specified
16015
+ if (!meta.image) {
16016
+ meta.image = generatePlaceholderAgentProfileImageUrl(parseResult.agentName || '!!');
16017
+ }
16018
+ // Parse parameters using unified approach - both @Parameter and {parameter} notations
16019
+ // are treated as the same syntax feature with unified representation
16020
+ const parameters = parseParameters(agentSource);
16021
+ const agentHash = computeAgentHash(agentSource);
16022
+ return {
16023
+ agentName: normalizeAgentName(parseResult.agentName || createDefaultAgentName(agentSource)),
16024
+ agentHash,
16025
+ personaDescription,
16026
+ meta,
16027
+ parameters,
16028
+ };
16029
+ }
16030
+ /**
16031
+ * TODO: [🕛] Unite `AgentBasicInformation`, `ChatParticipant`, `LlmExecutionTools` + `LlmToolsMetadata`
16032
+ */
16033
+
16034
+ /**
16035
+ * Creates model requirements for an agent based on its source
16036
+ *
16037
+ * There are 2 similar functions:
16038
+ * - `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.
16039
+ * - `createAgentModelRequirements` which is an asynchronous function that creates model requirements it applies each commitment one by one and works asynchronous.
16040
+ *
16041
+ * @public exported from `@promptbook/core`
16042
+ */
16043
+ async function createAgentModelRequirements(agentSource, modelName, availableModels, llmTools) {
16044
+ // If availableModels are provided and no specific modelName is given,
16045
+ // use preparePersona to select the best model
16046
+ if (availableModels && !modelName && llmTools) {
16047
+ const selectedModelName = await selectBestModelUsingPersona(agentSource, llmTools);
16048
+ return createAgentModelRequirementsWithCommitments(agentSource, selectedModelName);
16049
+ }
16050
+ // Use the new commitment-based system with provided or default model
16051
+ return createAgentModelRequirementsWithCommitments(agentSource, modelName);
16052
+ }
16053
+ /**
16054
+ * Selects the best model using the preparePersona function
16055
+ * This directly uses preparePersona to ensure DRY principle
16056
+ *
16057
+ * @param agentSource The agent source to derive persona description from
16058
+ * @param llmTools LLM tools for preparing persona
16059
+ * @returns The name of the best selected model
16060
+ * @private function of `createAgentModelRequirements`
16061
+ */
16062
+ async function selectBestModelUsingPersona(agentSource, llmTools) {
16063
+ var _a;
16064
+ // Parse agent source to get persona description
16065
+ const { agentName, personaDescription } = parseAgentSource(agentSource);
16066
+ // Use agent name as fallback if no persona description is available
16067
+ const description = personaDescription || agentName || 'AI Agent';
16068
+ try {
16069
+ // Use preparePersona directly
16070
+ const { modelsRequirements } = await preparePersona(description, { llm: llmTools }, { isVerbose: false });
16071
+ // Extract the first model name from the requirements
16072
+ if (modelsRequirements.length > 0 && ((_a = modelsRequirements[0]) === null || _a === void 0 ? void 0 : _a.modelName)) {
16073
+ return modelsRequirements[0].modelName;
16074
+ }
16075
+ // Fallback: get available models and return the first CHAT model
16076
+ const availableModels = await llmTools.listModels();
16077
+ const chatModels = availableModels.filter(({ modelVariant }) => modelVariant === 'CHAT');
16078
+ if (chatModels.length === 0) {
16079
+ throw new Error('No CHAT models available for agent model selection');
16080
+ }
16081
+ return chatModels[0].modelName;
16082
+ }
16083
+ catch (error) {
16084
+ console.warn('Failed to use preparePersona for model selection, falling back to first available model:', error);
16085
+ // Fallback: get available models and return the first CHAT model
16086
+ const availableModels = await llmTools.listModels();
16087
+ const chatModels = availableModels.filter(({ modelVariant }) => modelVariant === 'CHAT');
16088
+ if (chatModels.length === 0) {
16089
+ throw new Error('No CHAT models available for agent model selection');
16090
+ }
16091
+ return chatModels[0].modelName;
16092
+ }
16093
+ }
16094
+ /**
16095
+ * Extracts MCP servers from agent source
16096
+ *
16097
+ * @param agentSource The agent source string that may contain MCP lines
16098
+ * @returns Array of MCP server identifiers
16099
+ *
16100
+ * @private TODO: [🧠] Maybe should be public
16101
+ */
16102
+ function extractMcpServers(agentSource) {
16103
+ if (!agentSource) {
16104
+ return [];
16105
+ }
16106
+ const lines = agentSource.split('\n');
16107
+ const mcpRegex = /^\s*MCP\s+(.+)$/i;
16108
+ const mcpServers = [];
16109
+ // Look for MCP lines
16110
+ for (const line of lines) {
16111
+ const match = line.match(mcpRegex);
16112
+ if (match && match[1]) {
16113
+ mcpServers.push(match[1].trim());
16114
+ }
16115
+ }
16116
+ return mcpServers;
16117
+ }
16118
+
16119
+ /**
16120
+ * Transpiler to Javascript code using OpenAI SDK.
16121
+ *
16122
+ * @public exported from `@promptbook/core`
16123
+ */
16124
+ const OpenAiSdkTranspiler = {
16125
+ name: 'openai-sdk',
16126
+ title: 'OpenAI SDK',
16127
+ packageName: '@promptbook/core',
16128
+ className: 'OpenAiSdkTranspiler',
16129
+ async transpileBook(book, tools, options) {
15681
16130
  const { agentName } = await parseAgentSource(book);
15682
16131
  const modelRequirements = await createAgentModelRequirements(book);
15683
16132
  const { commitments } = parseAgentSourceWithCommitments(book);
@@ -15792,7 +16241,7 @@ const OpenAiSdkTranspiler = {
15792
16241
  });
15793
16242
 
15794
16243
  const answer = response.choices[0].message.content;
15795
- console.log('\\n🧠 ${agentName}:', answer, '\\n');
16244
+ console.log('\\n🧠 ${agentName /* <- TODO: [🕛] There should be `agentFullname` not `agentName` */}:', answer, '\\n');
15796
16245
 
15797
16246
  chatHistory.push({ role: 'assistant', content: answer });
15798
16247
  promptUser();
@@ -15811,7 +16260,7 @@ const OpenAiSdkTranspiler = {
15811
16260
 
15812
16261
  (async () => {
15813
16262
  await setupKnowledge();
15814
- console.log("🤖 Chat with ${agentName} (type 'exit' to quit)\\n");
16263
+ console.log("🤖 Chat with ${agentName /* <- TODO: [🕛] There should be `agentFullname` not `agentName` */} (type 'exit' to quit)\\n");
15815
16264
  promptUser();
15816
16265
  })();
15817
16266
  `);
@@ -15858,7 +16307,7 @@ const OpenAiSdkTranspiler = {
15858
16307
  });
15859
16308
 
15860
16309
  const answer = response.choices[0].message.content;
15861
- console.log('\\n🧠 ${agentName}:', answer, '\\n');
16310
+ console.log('\\n🧠 ${agentName /* <- TODO: [🕛] There should be `agentFullname` not `agentName` */}:', answer, '\\n');
15862
16311
 
15863
16312
  chatHistory.push({ role: 'assistant', content: answer });
15864
16313
  promptUser();
@@ -15875,7 +16324,7 @@ const OpenAiSdkTranspiler = {
15875
16324
  });
15876
16325
  }
15877
16326
 
15878
- console.log("🤖 Chat with ${agentName} (type 'exit' to quit)\\n");
16327
+ console.log("🤖 Chat with ${agentName /* <- TODO: [🕛] There should be `agentFullname` not `agentName` */} (type 'exit' to quit)\\n");
15879
16328
  promptUser();
15880
16329
 
15881
16330
  `);
@@ -15891,7 +16340,7 @@ const OpenAiSdkTranspiler = {
15891
16340
  * @public exported from `@promptbook/wizard`
15892
16341
  * @public exported from `@promptbook/cli`
15893
16342
  *
15894
- * TODO: !!!! Which package should export this?
16343
+ * TODO: [🧠] Which package should export this?
15895
16344
  */
15896
16345
  const _OpenAiSdkTranspilerRegistration = $bookTranspilersRegister.register(OpenAiSdkTranspiler);
15897
16346
  /**
@@ -16164,25 +16613,6 @@ function promptbookTokenToIdentification(promptbookToken) {
16164
16613
  return identification;
16165
16614
  }
16166
16615
 
16167
- /**
16168
- * Determines if the given path is a root path.
16169
- *
16170
- * Note: This does not check if the file exists only if the path is valid
16171
- * @public exported from `@promptbook/utils`
16172
- */
16173
- function isRootPath(value) {
16174
- if (value === '/') {
16175
- return true;
16176
- }
16177
- if (/^[A-Z]:\\$/i.test(value)) {
16178
- return true;
16179
- }
16180
- return false;
16181
- }
16182
- /**
16183
- * TODO: [🍏] Make for MacOS paths
16184
- */
16185
-
16186
16616
  /**
16187
16617
  * Path to the `.env` file which was used to configure LLM tools
16188
16618
  *
@@ -16448,41 +16878,7 @@ class $EnvStorage {
16448
16878
  }
16449
16879
  }
16450
16880
  /**
16451
- * TODO: Write file more securely - ensure that there can be no accidental overwriting of existing variables and other content
16452
- */
16453
-
16454
- // <- TODO: Auto convert to type `import { ... } from 'type-fest';`
16455
- /**
16456
- * Tests if the value is [🚉] serializable as JSON
16457
- *
16458
- * - Almost all primitives are serializable BUT:
16459
- * - `undefined` is not serializable
16460
- * - `NaN` is not serializable
16461
- * - Objects and arrays are serializable if all their properties are serializable
16462
- * - Functions are not serializable
16463
- * - Circular references are not serializable
16464
- * - `Date` objects are not serializable
16465
- * - `Map` and `Set` objects are not serializable
16466
- * - `RegExp` objects are not serializable
16467
- * - `Error` objects are not serializable
16468
- * - `Symbol` objects are not serializable
16469
- * - And much more...
16470
- *
16471
- *
16472
- * @public exported from `@promptbook/utils`
16473
- */
16474
- function isSerializableAsJson(value) {
16475
- try {
16476
- checkSerializableAsJson({ value });
16477
- return true;
16478
- }
16479
- catch (error) {
16480
- return false;
16481
- }
16482
- }
16483
- /**
16484
- * TODO: [🧠][main] !!3 In-memory cache of same values to prevent multiple checks
16485
- * TODO: [🧠][💺] Can be done this on type-level?
16881
+ * TODO: Write file more securely - ensure that there can be no accidental overwriting of existing variables and other content
16486
16882
  */
16487
16883
 
16488
16884
  /**
@@ -16760,582 +17156,323 @@ function cacheLlmTools(llmTools, options = {}) {
16760
17156
  });
16761
17157
  shouldCache = validationResult.isValid;
16762
17158
  if (!shouldCache && isVerbose) {
16763
- console.info('Not caching result that fails expectations/format validation for key:', key, {
16764
- content: promptResult.content,
16765
- expectations: prompt.expectations,
16766
- format: prompt.format,
16767
- validationError: (_a = validationResult.error) === null || _a === void 0 ? void 0 : _a.message,
16768
- });
16769
- }
16770
- }
16771
- catch (error) {
16772
- // If validation throws an unexpected error, don't cache
16773
- shouldCache = false;
16774
- if (isVerbose) {
16775
- console.info('Not caching result due to validation error for key:', key, {
16776
- content: promptResult.content,
16777
- validationError: error instanceof Error ? error.message : String(error),
16778
- });
16779
- }
16780
- }
16781
- }
16782
- if (shouldCache) {
16783
- await storage.setItem(key, {
16784
- date: $getCurrentDate(),
16785
- promptbookVersion: PROMPTBOOK_ENGINE_VERSION,
16786
- bookVersion: BOOK_LANGUAGE_VERSION,
16787
- prompt: {
16788
- ...prompt,
16789
- parameters: Object.entries(parameters).length === Object.entries(relevantParameters).length
16790
- ? parameters
16791
- : {
16792
- ...relevantParameters,
16793
- note: `<- Note: Only relevant parameters are stored in the cache`,
16794
- },
16795
- },
16796
- promptResult,
16797
- });
16798
- }
16799
- else if (isVerbose && isBasicFailedResult) {
16800
- console.info('Not caching failed result for key:', key, {
16801
- content: promptResult.content,
16802
- error: promptResult.error,
16803
- success: promptResult.success,
16804
- });
16805
- }
16806
- return promptResult;
16807
- };
16808
- if (llmTools.callChatModel !== undefined) {
16809
- proxyTools.callChatModel = async (prompt) => {
16810
- return /* not await */ callCommonModel(prompt);
16811
- };
16812
- }
16813
- if (llmTools.callCompletionModel !== undefined) {
16814
- proxyTools.callCompletionModel = async (prompt) => {
16815
- return /* not await */ callCommonModel(prompt);
16816
- };
16817
- }
16818
- if (llmTools.callEmbeddingModel !== undefined) {
16819
- proxyTools.callEmbeddingModel = async (prompt) => {
16820
- return /* not await */ callCommonModel(prompt);
16821
- };
16822
- }
16823
- // <- Note: [🤖]
16824
- return proxyTools;
16825
- }
16826
- /**
16827
- * TODO: [🧠][💸] Maybe make some common abstraction `interceptLlmTools` and use here (or use javascript Proxy?)
16828
- * TODO: [🧠] Is there some meaningfull way how to test this util
16829
- * TODO: [👷‍♂️] Comprehensive manual about construction of llmTools
16830
- * Detailed explanation about caching strategies and appropriate storage selection for different use cases
16831
- * Examples of how to combine multiple interceptors for advanced caching, logging, and usage tracking
16832
- */
16833
-
16834
- /**
16835
- * Provides LLM tools configuration by reading environment variables.
16836
- *
16837
- * Note: `$` is used to indicate that this function is not a pure function - it uses filesystem to access `.env` file
16838
- *
16839
- * It looks for environment variables:
16840
- * - `process.env.OPENAI_API_KEY`
16841
- * - `process.env.ANTHROPIC_CLAUDE_API_KEY`
16842
- * - ...
16843
- *
16844
- * @see Environment variables documentation or .env file for required variables.
16845
- * @returns A promise that resolves to the LLM tools configuration, or null if configuration is incomplete or missing.
16846
- * @public exported from `@promptbook/node`
16847
- */
16848
- async function $provideLlmToolsConfigurationFromEnv() {
16849
- if (!$isRunningInNode()) {
16850
- throw new EnvironmentMismatchError('Function `$provideLlmToolsFromEnv` works only in Node.js environment');
16851
- }
16852
- const envFilepath = await $provideEnvFilename();
16853
- if (envFilepath !== null) {
16854
- dotenv.config({ path: envFilepath });
16855
- }
16856
- const llmToolsConfiguration = $llmToolsMetadataRegister
16857
- .list()
16858
- .map((metadata) => metadata.createConfigurationFromEnv(process.env))
16859
- .filter((configuration) => configuration !== null);
16860
- return llmToolsConfiguration;
16861
- }
16862
- /**
16863
- * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
16864
- */
16865
-
16866
- /**
16867
- * Creates LLM execution tools from provided configuration objects
16868
- *
16869
- * Instantiates and configures LLM tool instances for each configuration entry,
16870
- * combining them into a unified interface via MultipleLlmExecutionTools.
16871
- *
16872
- * Note: This function is not cached, every call creates new instance of `MultipleLlmExecutionTools`
16873
- *
16874
- * @param configuration Array of LLM tool configurations to instantiate
16875
- * @param options Additional options for configuring the LLM tools
16876
- * @returns A unified interface combining all successfully instantiated LLM tools
16877
- * @public exported from `@promptbook/core`
16878
- */
16879
- function createLlmToolsFromConfiguration(configuration, options = {}) {
16880
- const { title = 'LLM Tools from Configuration', isVerbose = DEFAULT_IS_VERBOSE, userId } = options;
16881
- const llmTools = configuration.map((llmConfiguration) => {
16882
- const registeredItem = $llmToolsRegister
16883
- .list()
16884
- .find(({ packageName, className }) => llmConfiguration.packageName === packageName && llmConfiguration.className === className);
16885
- if (registeredItem === undefined) {
16886
- // console.log('$llmToolsRegister.list()', $llmToolsRegister.list());
16887
- throw new Error(spaceTrim((block) => `
16888
- There is no constructor for LLM provider \`${llmConfiguration.className}\` from \`${llmConfiguration.packageName}\`
16889
- Running in ${!$isRunningInBrowser() ? '' : 'browser environment'}${!$isRunningInNode() ? '' : 'node environment'}${!$isRunningInWebWorker() ? '' : 'worker environment'}
16890
-
16891
- You have probably forgotten install and import the provider package.
16892
- To fix this issue, you can:
16893
-
16894
- Install:
16895
-
16896
- > npm install ${llmConfiguration.packageName}
16897
-
16898
- And import:
16899
-
16900
- > import '${llmConfiguration.packageName}';
16901
-
16902
-
16903
- ${block($registeredLlmToolsMessage())}
16904
- `));
16905
- }
16906
- return registeredItem({
16907
- isVerbose,
16908
- userId,
16909
- ...llmConfiguration.options,
16910
- });
16911
- });
16912
- return joinLlmExecutionTools(title, ...llmTools);
16913
- }
16914
- /**
16915
- * TODO: [🎌] Together with `createLlmToolsFromConfiguration` + 'EXECUTION_TOOLS_CLASSES' gets to `@promptbook/core` ALL model providers, make this more efficient
16916
- * TODO: [🧠][🎌] Dynamically install required providers
16917
- * TODO: We should implement an interactive configuration wizard that would:
16918
- * 1. Detect which LLM providers are available in the environment
16919
- * 2. Guide users through required configuration settings for each provider
16920
- * 3. Allow testing connections before completing setup
16921
- * 4. Generate appropriate configuration code for application integration
16922
- * TODO: [🧠][🍛] Which name is better `createLlmToolsFromConfig` or `createLlmToolsFromConfiguration`?
16923
- * TODO: [🧠] Is there some meaningfull way how to test this util
16924
- * TODO: This should be maybe not under `_common` but under `utils`
16925
- * TODO: [®] DRY Register logic
16926
- */
16927
-
16928
- /**
16929
- * Automatically configures LLM tools from environment variables in Node.js
16930
- *
16931
- * This utility function detects available LLM providers based on environment variables
16932
- * and creates properly configured LLM execution tools for each detected provider.
16933
- *
16934
- * Note: This function is not cached, every call creates new instance of `MultipleLlmExecutionTools`
16935
- *
16936
- * Supports environment variables from .env files when dotenv is configured
16937
- * Note: `$` is used to indicate that this function is not a pure function - it uses filesystem to access `.env` file
16938
- *
16939
- * It looks for environment variables:
16940
- * - `process.env.OPENAI_API_KEY`
16941
- * - `process.env.ANTHROPIC_CLAUDE_API_KEY`
16942
- * - ...
16943
- *
16944
- * @param options Configuration options for the LLM tools
16945
- * @returns A unified interface containing all detected and configured LLM tools
16946
- * @public exported from `@promptbook/node`
16947
- */
16948
- async function $provideLlmToolsFromEnv(options = {}) {
16949
- if (!$isRunningInNode()) {
16950
- throw new EnvironmentMismatchError('Function `$provideLlmToolsFromEnv` works only in Node.js environment');
16951
- }
16952
- const configuration = await $provideLlmToolsConfigurationFromEnv();
16953
- if (configuration.length === 0) {
16954
- if ($llmToolsMetadataRegister.list().length === 0) {
16955
- throw new UnexpectedError(spaceTrim((block) => `
16956
- No LLM tools registered, this is probably a bug in the Promptbook library
16957
-
16958
- ${block($registeredLlmToolsMessage())}}
16959
- `));
16960
- }
16961
- // TODO: [🥃]
16962
- throw new Error(spaceTrim((block) => `
16963
- No LLM tools found in the environment
16964
-
16965
- ${block($registeredLlmToolsMessage())}}
16966
- `));
16967
- }
16968
- return createLlmToolsFromConfiguration(configuration, options);
16969
- }
16970
- /**
16971
- * TODO: The architecture for LLM tools configuration consists of three key functions:
16972
- * 1. `$provideLlmToolsFromEnv` - High-level function that detects available providers from env vars and returns ready-to-use LLM tools
16973
- * 2. `$provideLlmToolsConfigurationFromEnv` - Middle layer that extracts configuration objects from environment variables
16974
- * 3. `createLlmToolsFromConfiguration` - Low-level function that instantiates LLM tools from explicit configuration
16975
- *
16976
- * This layered approach allows flexibility in how tools are configured:
16977
- * - Use $provideLlmToolsFromEnv for automatic detection and setup in Node.js environments
16978
- * - Use $provideLlmToolsConfigurationFromEnv to extract config objects for modification before instantiation
16979
- * - Use createLlmToolsFromConfiguration for explicit control over tool configurations
16980
- *
16981
- * TODO: [🧠][🍛] Which name is better `$provideLlmToolsFromEnv` or `$provideLlmToolsFromEnvironment`?
16982
- * TODO: [🧠] Is there some meaningfull way how to test this util
16983
- * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
16984
- * TODO: [🥃] Allow `ptbk make` without llm tools
16985
- * TODO: This should be maybe not under `_common` but under `utils`
16986
- * TODO: [®] DRY Register logic
16987
- */
16988
-
16989
- /**
16990
- * Returns LLM tools for CLI
16991
- *
16992
- * 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
16993
- *
16994
- * @private within the repository - for CLI utils
16995
- */
16996
- async function $provideLlmToolsForWizardOrCli(options) {
16997
- if (!$isRunningInNode()) {
16998
- throw new EnvironmentMismatchError('Function `$provideLlmToolsForWizardOrCli` works only in Node.js environment');
16999
- }
17000
- options = options !== null && options !== void 0 ? options : { strategy: 'BRING_YOUR_OWN_KEYS' };
17001
- const { isLoginloaded, strategy, isCacheReloaded } = options;
17002
- let llmExecutionTools;
17003
- if (strategy === 'REMOTE_SERVER') {
17004
- const { remoteServerUrl = DEFAULT_REMOTE_SERVER_URL, loginPrompt } = options;
17005
- const storage = new $EnvStorage();
17006
- let key = `PROMPTBOOK_TOKEN`;
17007
- if (remoteServerUrl !== DEFAULT_REMOTE_SERVER_URL) {
17008
- key = `${key}_${remoteServerUrl.replace(/^https?:\/\//i, '')}`;
17009
- }
17010
- let identification = null;
17011
- let promptbookToken = await storage.getItem(key);
17012
- if (promptbookToken === null || isLoginloaded) {
17013
- identification = await loginPrompt();
17014
- // Note: When login prompt fails, `process.exit(1)` is called so no need to check for null
17015
- if (identification.isAnonymous === false) {
17016
- promptbookToken = identificationToPromptbookToken(identification);
17017
- await storage.setItem(key, promptbookToken);
17159
+ console.info('Not caching result that fails expectations/format validation for key:', key, {
17160
+ content: promptResult.content,
17161
+ expectations: prompt.expectations,
17162
+ format: prompt.format,
17163
+ validationError: (_a = validationResult.error) === null || _a === void 0 ? void 0 : _a.message,
17164
+ });
17165
+ }
17166
+ }
17167
+ catch (error) {
17168
+ // If validation throws an unexpected error, don't cache
17169
+ shouldCache = false;
17170
+ if (isVerbose) {
17171
+ console.info('Not caching result due to validation error for key:', key, {
17172
+ content: promptResult.content,
17173
+ validationError: error instanceof Error ? error.message : String(error),
17174
+ });
17175
+ }
17018
17176
  }
17019
17177
  }
17020
- else {
17021
- identification = promptbookTokenToIdentification(promptbookToken);
17178
+ if (shouldCache) {
17179
+ await storage.setItem(key, {
17180
+ date: $getCurrentDate(),
17181
+ promptbookVersion: PROMPTBOOK_ENGINE_VERSION,
17182
+ bookVersion: BOOK_LANGUAGE_VERSION,
17183
+ prompt: {
17184
+ ...prompt,
17185
+ parameters: Object.entries(parameters).length === Object.entries(relevantParameters).length
17186
+ ? parameters
17187
+ : {
17188
+ ...relevantParameters,
17189
+ note: `<- Note: Only relevant parameters are stored in the cache`,
17190
+ },
17191
+ },
17192
+ promptResult,
17193
+ });
17022
17194
  }
17023
- llmExecutionTools = new RemoteLlmExecutionTools({
17024
- remoteServerUrl,
17025
- identification,
17026
- });
17195
+ else if (isVerbose && isBasicFailedResult) {
17196
+ console.info('Not caching failed result for key:', key, {
17197
+ content: promptResult.content,
17198
+ error: promptResult.error,
17199
+ success: promptResult.success,
17200
+ });
17201
+ }
17202
+ return promptResult;
17203
+ };
17204
+ if (llmTools.callChatModel !== undefined) {
17205
+ proxyTools.callChatModel = async (prompt) => {
17206
+ return /* not await */ callCommonModel(prompt);
17207
+ };
17027
17208
  }
17028
- else if (strategy === 'BRING_YOUR_OWN_KEYS') {
17029
- llmExecutionTools = await $provideLlmToolsFromEnv({
17030
- title: 'LLM Tools for wizard or CLI with BYOK strategy',
17031
- });
17209
+ if (llmTools.callCompletionModel !== undefined) {
17210
+ proxyTools.callCompletionModel = async (prompt) => {
17211
+ return /* not await */ callCommonModel(prompt);
17212
+ };
17032
17213
  }
17033
- else {
17034
- throw new UnexpectedError(`\`$provideLlmToolsForWizardOrCli\` wrong strategy "${strategy}"`);
17214
+ if (llmTools.callEmbeddingModel !== undefined) {
17215
+ proxyTools.callEmbeddingModel = async (prompt) => {
17216
+ return /* not await */ callCommonModel(prompt);
17217
+ };
17035
17218
  }
17036
- return cacheLlmTools(countUsage(
17037
- // <- TODO: [🌯] We dont use countUsage at all, maybe just unwrap it
17038
- // <- Note: for example here we don`t want the [🌯]
17039
- llmExecutionTools), {
17040
- storage: new FileCacheStorage({ fs: $provideFilesystemForNode() }, {
17041
- rootFolderPath: join(process.cwd(), DEFAULT_EXECUTION_CACHE_DIRNAME),
17042
- }),
17043
- isCacheReloaded,
17044
- });
17219
+ // <- Note: [🤖]
17220
+ return proxyTools;
17045
17221
  }
17046
17222
  /**
17047
- * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
17048
- * TODO: [👷‍♂️] @@@ Manual about construction of llmTools
17049
- * TODO: [🥃] Allow `ptbk make` without llm tools
17050
- * TODO: This should be maybe not under `_common` but under `utils-internal` / `utils/internal`
17051
- * TODO: [®] DRY Register logic
17223
+ * TODO: [🧠][💸] Maybe make some common abstraction `interceptLlmTools` and use here (or use javascript Proxy?)
17224
+ * TODO: [🧠] Is there some meaningfull way how to test this util
17225
+ * TODO: [👷‍♂️] Comprehensive manual about construction of llmTools
17226
+ * Detailed explanation about caching strategies and appropriate storage selection for different use cases
17227
+ * Examples of how to combine multiple interceptors for advanced caching, logging, and usage tracking
17052
17228
  */
17053
17229
 
17054
17230
  /**
17055
- * Provides a collection of scrapers optimized for Node.js environment.
17056
- * 1) `provideScrapersForNode` use as default
17057
- * 2) `provideScrapersForBrowser` use in limited browser environment *
17231
+ * Provides LLM tools configuration by reading environment variables.
17232
+ *
17233
+ * Note: `$` is used to indicate that this function is not a pure function - it uses filesystem to access `.env` file
17234
+ *
17235
+ * It looks for environment variables:
17236
+ * - `process.env.OPENAI_API_KEY`
17237
+ * - `process.env.ANTHROPIC_CLAUDE_API_KEY`
17238
+ * - ...
17239
+ *
17240
+ * @see Environment variables documentation or .env file for required variables.
17241
+ * @returns A promise that resolves to the LLM tools configuration, or null if configuration is incomplete or missing.
17058
17242
  * @public exported from `@promptbook/node`
17059
17243
  */
17060
- async function $provideScrapersForNode(tools, options) {
17244
+ async function $provideLlmToolsConfigurationFromEnv() {
17061
17245
  if (!$isRunningInNode()) {
17062
- throw new EnvironmentMismatchError('Function `$getScrapersForNode` works only in Node.js environment');
17246
+ throw new EnvironmentMismatchError('Function `$provideLlmToolsFromEnv` works only in Node.js environment');
17063
17247
  }
17064
- // TODO: [🔱] Do here auto-installation + auto-include of missing scrapers - use all from $scrapersMetadataRegister.list()
17065
- // TODO: [🔱][🧠] What is the best strategy for auto-install - install them all?
17066
- const scrapers = [];
17067
- for (const scraperFactory of $scrapersRegister.list()) {
17068
- const scraper = await scraperFactory(tools, options || {});
17069
- if (scraper.metadata.packageName === '@promptbook/boilerplate' ||
17070
- scraper.metadata.mimeTypes.some((mimeType) => mimeType.includes('DISABLED'))) {
17071
- continue;
17072
- }
17073
- scrapers.push(scraper);
17248
+ const envFilepath = await $provideEnvFilename();
17249
+ if (envFilepath !== null) {
17250
+ dotenv.config({ path: envFilepath });
17074
17251
  }
17075
- return scrapers;
17252
+ const llmToolsConfiguration = $llmToolsMetadataRegister
17253
+ .list()
17254
+ .map((metadata) => metadata.createConfigurationFromEnv(process.env))
17255
+ .filter((configuration) => configuration !== null);
17256
+ return llmToolsConfiguration;
17076
17257
  }
17077
17258
  /**
17078
17259
  * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
17079
17260
  */
17080
17261
 
17081
17262
  /**
17082
- * Function parseNumber will parse number from string
17263
+ * Creates LLM execution tools from provided configuration objects
17083
17264
  *
17084
- * Note: [🔂] This function is idempotent.
17085
- * Unlike Number.parseInt, Number.parseFloat it will never ever result in NaN
17086
- * Note: it also works only with decimal numbers
17265
+ * Instantiates and configures LLM tool instances for each configuration entry,
17266
+ * combining them into a unified interface via MultipleLlmExecutionTools.
17087
17267
  *
17088
- * @returns parsed number
17089
- * @throws {ParseError} if the value is not a number
17268
+ * Note: This function is not cached, every call creates new instance of `MultipleLlmExecutionTools`
17090
17269
  *
17091
- * @public exported from `@promptbook/utils`
17092
- */
17093
- function parseNumber(value) {
17094
- const originalValue = value;
17095
- if (typeof value === 'number') {
17096
- value = value.toString(); // <- TODO: Maybe more efficient way to do this
17097
- }
17098
- if (typeof value !== 'string') {
17099
- return 0;
17100
- }
17101
- value = value.trim();
17102
- if (value.startsWith('+')) {
17103
- return parseNumber(value.substring(1));
17104
- }
17105
- if (value.startsWith('-')) {
17106
- const number = parseNumber(value.substring(1));
17107
- if (number === 0) {
17108
- return 0; // <- Note: To prevent -0
17109
- }
17110
- return -number;
17111
- }
17112
- value = value.replace(/,/g, '.');
17113
- value = value.toUpperCase();
17114
- if (value === '') {
17115
- return 0;
17116
- }
17117
- if (value === '♾' || value.startsWith('INF')) {
17118
- return Infinity;
17119
- }
17120
- if (value.includes('/')) {
17121
- const [numerator_, denominator_] = value.split('/');
17122
- const numerator = parseNumber(numerator_);
17123
- const denominator = parseNumber(denominator_);
17124
- if (denominator === 0) {
17125
- throw new ParseError(`Unable to parse number from "${originalValue}" because denominator is zero`);
17126
- }
17127
- return numerator / denominator;
17128
- }
17129
- if (/^(NAN|NULL|NONE|UNDEFINED|ZERO|NO.*)$/.test(value)) {
17130
- return 0;
17131
- }
17132
- if (value.includes('E')) {
17133
- const [significand, exponent] = value.split('E');
17134
- return parseNumber(significand) * 10 ** parseNumber(exponent);
17135
- }
17136
- if (!/^[0-9.]+$/.test(value) || value.split('.').length > 2) {
17137
- throw new ParseError(`Unable to parse number from "${originalValue}"`);
17138
- }
17139
- const num = parseFloat(value);
17140
- if (isNaN(num)) {
17141
- throw new ParseError(`Unexpected NaN when parsing number from "${originalValue}"`);
17142
- }
17143
- return num;
17144
- }
17145
- /**
17146
- * TODO: Maybe use sth. like safe-eval in fraction/calculation case @see https://www.npmjs.com/package/safe-eval
17147
- * TODO: [🧠][🌻] Maybe export through `@promptbook/markdown-utils` not `@promptbook/utils`
17270
+ * @param configuration Array of LLM tool configurations to instantiate
17271
+ * @param options Additional options for configuring the LLM tools
17272
+ * @returns A unified interface combining all successfully instantiated LLM tools
17273
+ * @public exported from `@promptbook/core`
17148
17274
  */
17275
+ function createLlmToolsFromConfiguration(configuration, options = {}) {
17276
+ const { title = 'LLM Tools from Configuration', isVerbose = DEFAULT_IS_VERBOSE, userId } = options;
17277
+ const llmTools = configuration.map((llmConfiguration) => {
17278
+ const registeredItem = $llmToolsRegister
17279
+ .list()
17280
+ .find(({ packageName, className }) => llmConfiguration.packageName === packageName && llmConfiguration.className === className);
17281
+ if (registeredItem === undefined) {
17282
+ // console.log('$llmToolsRegister.list()', $llmToolsRegister.list());
17283
+ throw new Error(spaceTrim((block) => `
17284
+ There is no constructor for LLM provider \`${llmConfiguration.className}\` from \`${llmConfiguration.packageName}\`
17285
+ Running in ${!$isRunningInBrowser() ? '' : 'browser environment'}${!$isRunningInNode() ? '' : 'node environment'}${!$isRunningInWebWorker() ? '' : 'worker environment'}
17149
17286
 
17150
- /**
17151
- * Makes first letter of a string lowercase
17152
- *
17153
- * Note: [🔂] This function is idempotent.
17154
- *
17155
- * @public exported from `@promptbook/utils`
17156
- */
17157
- function decapitalize(word) {
17158
- return word.substring(0, 1).toLowerCase() + word.substring(1);
17159
- }
17287
+ You have probably forgotten install and import the provider package.
17288
+ To fix this issue, you can:
17160
17289
 
17161
- /**
17162
- * Parses keywords from a string
17163
- *
17164
- * @param {string} input
17165
- * @returns {Set} of keywords without diacritics in lowercase
17166
- * @public exported from `@promptbook/utils`
17167
- */
17168
- function parseKeywordsFromString(input) {
17169
- const keywords = normalizeTo_SCREAMING_CASE(removeDiacritics(input))
17170
- .toLowerCase()
17171
- .split(/[^a-z0-9]+/gs)
17172
- .filter((value) => value);
17173
- return new Set(keywords);
17174
- }
17290
+ Install:
17291
+
17292
+ > npm install ${llmConfiguration.packageName}
17293
+
17294
+ And import:
17295
+
17296
+ > import '${llmConfiguration.packageName}';
17175
17297
 
17176
- /**
17177
- * Converts a name string into a URI-compatible format.
17178
- *
17179
- * @param name The string to be converted to a URI-compatible format.
17180
- * @returns A URI-compatible string derived from the input name.
17181
- * @example 'Hello World' -> 'hello-world'
17182
- * @public exported from `@promptbook/utils`
17183
- */
17184
- function nameToUriPart(name) {
17185
- let uriPart = name;
17186
- uriPart = uriPart.toLowerCase();
17187
- uriPart = removeDiacritics(uriPart);
17188
- uriPart = uriPart.replace(/[^a-zA-Z0-9]+/g, '-');
17189
- uriPart = uriPart.replace(/^-+/, '');
17190
- uriPart = uriPart.replace(/-+$/, '');
17191
- return uriPart;
17192
- }
17193
17298
 
17194
- /**
17195
- * Converts a given name into URI-compatible parts.
17196
- *
17197
- * @param name The name to be converted into URI parts.
17198
- * @returns An array of URI-compatible parts derived from the name.
17199
- * @example 'Example Name' -> ['example', 'name']
17200
- * @public exported from `@promptbook/utils`
17201
- */
17202
- function nameToUriParts(name) {
17203
- return nameToUriPart(name)
17204
- .split('-')
17205
- .filter((value) => value !== '');
17299
+ ${block($registeredLlmToolsMessage())}
17300
+ `));
17301
+ }
17302
+ return registeredItem({
17303
+ isVerbose,
17304
+ userId,
17305
+ ...llmConfiguration.options,
17306
+ });
17307
+ });
17308
+ return joinLlmExecutionTools(title, ...llmTools);
17206
17309
  }
17207
-
17208
17310
  /**
17209
- *
17210
- * @param text @public exported from `@promptbook/utils`
17211
- * @returns
17212
- * @example 'HelloWorld'
17213
- * @example 'ILovePromptbook'
17214
- * @public exported from `@promptbook/utils`
17311
+ * TODO: [🎌] Together with `createLlmToolsFromConfiguration` + 'EXECUTION_TOOLS_CLASSES' gets to `@promptbook/core` ALL model providers, make this more efficient
17312
+ * TODO: [🧠][🎌] Dynamically install required providers
17313
+ * TODO: We should implement an interactive configuration wizard that would:
17314
+ * 1. Detect which LLM providers are available in the environment
17315
+ * 2. Guide users through required configuration settings for each provider
17316
+ * 3. Allow testing connections before completing setup
17317
+ * 4. Generate appropriate configuration code for application integration
17318
+ * TODO: [🧠][🍛] Which name is better `createLlmToolsFromConfig` or `createLlmToolsFromConfiguration`?
17319
+ * TODO: [🧠] Is there some meaningfull way how to test this util
17320
+ * TODO: This should be maybe not under `_common` but under `utils`
17321
+ * TODO: [®] DRY Register logic
17215
17322
  */
17216
- function normalizeTo_PascalCase(text) {
17217
- return normalizeTo_camelCase(text, true);
17218
- }
17219
17323
 
17220
17324
  /**
17221
- * Take every whitespace (space, new line, tab) and replace it with a single space
17325
+ * Automatically configures LLM tools from environment variables in Node.js
17222
17326
  *
17223
- * Note: [🔂] This function is idempotent.
17327
+ * This utility function detects available LLM providers based on environment variables
17328
+ * and creates properly configured LLM execution tools for each detected provider.
17224
17329
  *
17225
- * @public exported from `@promptbook/utils`
17226
- */
17227
- function normalizeWhitespaces(sentence) {
17228
- return sentence.replace(/\s+/gs, ' ').trim();
17229
- }
17230
-
17231
- /**
17232
- * Removes quotes from a string
17330
+ * Note: This function is not cached, every call creates new instance of `MultipleLlmExecutionTools`
17233
17331
  *
17234
- * Note: [🔂] This function is idempotent.
17235
- * Tip: This is very useful for post-processing of the result of the LLM model
17236
- * Note: This function removes only the same quotes from the beginning and the end of the string
17237
- * Note: There are two similar functions:
17238
- * - `removeQuotes` which removes only bounding quotes
17239
- * - `unwrapResult` which removes whole introduce sentence
17332
+ * Supports environment variables from .env files when dotenv is configured
17333
+ * Note: `$` is used to indicate that this function is not a pure function - it uses filesystem to access `.env` file
17240
17334
  *
17241
- * @param text optionally quoted text
17242
- * @returns text without quotes
17243
- * @public exported from `@promptbook/utils`
17335
+ * It looks for environment variables:
17336
+ * - `process.env.OPENAI_API_KEY`
17337
+ * - `process.env.ANTHROPIC_CLAUDE_API_KEY`
17338
+ * - ...
17339
+ *
17340
+ * @param options Configuration options for the LLM tools
17341
+ * @returns A unified interface containing all detected and configured LLM tools
17342
+ * @public exported from `@promptbook/node`
17244
17343
  */
17245
- function removeQuotes(text) {
17246
- if (text.startsWith('"') && text.endsWith('"')) {
17247
- return text.slice(1, -1);
17344
+ async function $provideLlmToolsFromEnv(options = {}) {
17345
+ if (!$isRunningInNode()) {
17346
+ throw new EnvironmentMismatchError('Function `$provideLlmToolsFromEnv` works only in Node.js environment');
17248
17347
  }
17249
- if (text.startsWith("'") && text.endsWith("'")) {
17250
- return text.slice(1, -1);
17348
+ const configuration = await $provideLlmToolsConfigurationFromEnv();
17349
+ if (configuration.length === 0) {
17350
+ if ($llmToolsMetadataRegister.list().length === 0) {
17351
+ throw new UnexpectedError(spaceTrim((block) => `
17352
+ No LLM tools registered, this is probably a bug in the Promptbook library
17353
+
17354
+ ${block($registeredLlmToolsMessage())}}
17355
+ `));
17356
+ }
17357
+ // TODO: [🥃]
17358
+ throw new Error(spaceTrim((block) => `
17359
+ No LLM tools found in the environment
17360
+
17361
+ ${block($registeredLlmToolsMessage())}}
17362
+ `));
17251
17363
  }
17252
- return text;
17364
+ return createLlmToolsFromConfiguration(configuration, options);
17253
17365
  }
17366
+ /**
17367
+ * TODO: The architecture for LLM tools configuration consists of three key functions:
17368
+ * 1. `$provideLlmToolsFromEnv` - High-level function that detects available providers from env vars and returns ready-to-use LLM tools
17369
+ * 2. `$provideLlmToolsConfigurationFromEnv` - Middle layer that extracts configuration objects from environment variables
17370
+ * 3. `createLlmToolsFromConfiguration` - Low-level function that instantiates LLM tools from explicit configuration
17371
+ *
17372
+ * This layered approach allows flexibility in how tools are configured:
17373
+ * - Use $provideLlmToolsFromEnv for automatic detection and setup in Node.js environments
17374
+ * - Use $provideLlmToolsConfigurationFromEnv to extract config objects for modification before instantiation
17375
+ * - Use createLlmToolsFromConfiguration for explicit control over tool configurations
17376
+ *
17377
+ * TODO: [🧠][🍛] Which name is better `$provideLlmToolsFromEnv` or `$provideLlmToolsFromEnvironment`?
17378
+ * TODO: [🧠] Is there some meaningfull way how to test this util
17379
+ * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
17380
+ * TODO: [🥃] Allow `ptbk make` without llm tools
17381
+ * TODO: This should be maybe not under `_common` but under `utils`
17382
+ * TODO: [®] DRY Register logic
17383
+ */
17254
17384
 
17255
17385
  /**
17256
- * Removes quotes and optional introduce text from a string
17386
+ * Returns LLM tools for CLI
17257
17387
  *
17258
- * Tip: This is very useful for post-processing of the result of the LLM model
17259
- * Note: This function trims the text and removes whole introduce sentence if it is present
17260
- * Note: There are two similar functions:
17261
- * - `removeQuotes` which removes only bounding quotes
17262
- * - `unwrapResult` which removes whole introduce sentence
17388
+ * 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
17263
17389
  *
17264
- * @param text optionally quoted text
17265
- * @returns text without quotes
17266
- * @public exported from `@promptbook/utils`
17390
+ * @private within the repository - for CLI utils
17267
17391
  */
17268
- function unwrapResult(text, options) {
17269
- const { isTrimmed = true, isIntroduceSentenceRemoved = true } = options || {};
17270
- let trimmedText = text;
17271
- // Remove leading and trailing spaces and newlines
17272
- if (isTrimmed) {
17273
- trimmedText = spaceTrim$1(trimmedText);
17274
- }
17275
- let processedText = trimmedText;
17276
- if (isIntroduceSentenceRemoved) {
17277
- const introduceSentenceRegex = /^[a-zěščřžýáíéúů:\s]*:\s*/i;
17278
- if (introduceSentenceRegex.test(text)) {
17279
- // Remove the introduce sentence and quotes by replacing it with an empty string
17280
- processedText = processedText.replace(introduceSentenceRegex, '');
17281
- }
17282
- processedText = spaceTrim$1(processedText);
17283
- }
17284
- if (processedText.length < 3) {
17285
- return trimmedText;
17286
- }
17287
- if (processedText.includes('\n')) {
17288
- return trimmedText;
17392
+ async function $provideLlmToolsForWizardOrCli(options) {
17393
+ if (!$isRunningInNode()) {
17394
+ throw new EnvironmentMismatchError('Function `$provideLlmToolsForWizardOrCli` works only in Node.js environment');
17289
17395
  }
17290
- // Remove the quotes by extracting the substring without the first and last characters
17291
- const unquotedText = processedText.slice(1, -1);
17292
- // Check if the text starts and ends with quotes
17293
- if ([
17294
- ['"', '"'],
17295
- ["'", "'"],
17296
- ['`', '`'],
17297
- ['*', '*'],
17298
- ['_', '_'],
17299
- ['„', '“'],
17300
- ['«', '»'] /* <- QUOTES to config */,
17301
- ].some(([startQuote, endQuote]) => {
17302
- if (!processedText.startsWith(startQuote)) {
17303
- return false;
17304
- }
17305
- if (!processedText.endsWith(endQuote)) {
17306
- return false;
17396
+ options = options !== null && options !== void 0 ? options : { strategy: 'BRING_YOUR_OWN_KEYS' };
17397
+ const { isLoginloaded, strategy, isCacheReloaded } = options;
17398
+ let llmExecutionTools;
17399
+ if (strategy === 'REMOTE_SERVER') {
17400
+ const { remoteServerUrl = DEFAULT_REMOTE_SERVER_URL, loginPrompt } = options;
17401
+ const storage = new $EnvStorage();
17402
+ let key = `PROMPTBOOK_TOKEN`;
17403
+ if (remoteServerUrl !== DEFAULT_REMOTE_SERVER_URL) {
17404
+ key = `${key}_${remoteServerUrl.replace(/^https?:\/\//i, '')}`;
17307
17405
  }
17308
- if (unquotedText.includes(startQuote) && !unquotedText.includes(endQuote)) {
17309
- return false;
17406
+ let identification = null;
17407
+ let promptbookToken = await storage.getItem(key);
17408
+ if (promptbookToken === null || isLoginloaded) {
17409
+ identification = await loginPrompt();
17410
+ // Note: When login prompt fails, `process.exit(1)` is called so no need to check for null
17411
+ if (identification.isAnonymous === false) {
17412
+ promptbookToken = identificationToPromptbookToken(identification);
17413
+ await storage.setItem(key, promptbookToken);
17414
+ }
17310
17415
  }
17311
- if (!unquotedText.includes(startQuote) && unquotedText.includes(endQuote)) {
17312
- return false;
17416
+ else {
17417
+ identification = promptbookTokenToIdentification(promptbookToken);
17313
17418
  }
17314
- return true;
17315
- })) {
17316
- return unwrapResult(unquotedText, { isTrimmed: false, isIntroduceSentenceRemoved: false });
17419
+ llmExecutionTools = new RemoteLlmExecutionTools({
17420
+ remoteServerUrl,
17421
+ identification,
17422
+ });
17423
+ }
17424
+ else if (strategy === 'BRING_YOUR_OWN_KEYS') {
17425
+ llmExecutionTools = await $provideLlmToolsFromEnv({
17426
+ title: 'LLM Tools for wizard or CLI with BYOK strategy',
17427
+ });
17317
17428
  }
17318
17429
  else {
17319
- return processedText;
17430
+ throw new UnexpectedError(`\`$provideLlmToolsForWizardOrCli\` wrong strategy "${strategy}"`);
17320
17431
  }
17432
+ return cacheLlmTools(countUsage(
17433
+ // <- TODO: [🌯] We dont use countUsage at all, maybe just unwrap it
17434
+ // <- Note: for example here we don`t want the [🌯]
17435
+ llmExecutionTools), {
17436
+ storage: new FileCacheStorage({ fs: $provideFilesystemForNode() }, {
17437
+ rootFolderPath: join(process.cwd(), DEFAULT_EXECUTION_CACHE_DIRNAME),
17438
+ }),
17439
+ isCacheReloaded,
17440
+ });
17321
17441
  }
17322
17442
  /**
17323
- * TODO: [🧠] Should this also unwrap the (parenthesis)
17443
+ * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
17444
+ * TODO: [👷‍♂️] @@@ Manual about construction of llmTools
17445
+ * TODO: [🥃] Allow `ptbk make` without llm tools
17446
+ * TODO: This should be maybe not under `_common` but under `utils-internal` / `utils/internal`
17447
+ * TODO: [®] DRY Register logic
17324
17448
  */
17325
17449
 
17326
17450
  /**
17327
- * Checks if the given value is a valid JavaScript identifier name.
17328
- *
17329
- * @param javascriptName The value to check for JavaScript identifier validity.
17330
- * @returns `true` if the value is a valid JavaScript name, false otherwise.
17331
- * @public exported from `@promptbook/utils`
17451
+ * Provides a collection of scrapers optimized for Node.js environment.
17452
+ * 1) `provideScrapersForNode` use as default
17453
+ * 2) `provideScrapersForBrowser` use in limited browser environment *
17454
+ * @public exported from `@promptbook/node`
17332
17455
  */
17333
- function isValidJavascriptName(javascriptName) {
17334
- if (typeof javascriptName !== 'string') {
17335
- return false;
17456
+ async function $provideScrapersForNode(tools, options) {
17457
+ if (!$isRunningInNode()) {
17458
+ throw new EnvironmentMismatchError('Function `$getScrapersForNode` works only in Node.js environment');
17336
17459
  }
17337
- return /^[a-zA-Z_$][0-9a-zA-Z_$]*$/i.test(javascriptName);
17460
+ // TODO: [🔱] Do here auto-installation + auto-include of missing scrapers - use all from $scrapersMetadataRegister.list()
17461
+ // TODO: [🔱][🧠] What is the best strategy for auto-install - install them all?
17462
+ const scrapers = [];
17463
+ for (const scraperFactory of $scrapersRegister.list()) {
17464
+ const scraper = await scraperFactory(tools, options || {});
17465
+ if (scraper.metadata.packageName === '@promptbook/boilerplate' ||
17466
+ scraper.metadata.mimeTypes.some((mimeType) => mimeType.includes('DISABLED'))) {
17467
+ continue;
17468
+ }
17469
+ scrapers.push(scraper);
17470
+ }
17471
+ return scrapers;
17338
17472
  }
17473
+ /**
17474
+ * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
17475
+ */
17339
17476
 
17340
17477
  /**
17341
17478
  * Extracts exactly ONE code block from markdown.