@promptbook/cli 0.98.0-5 โ†’ 0.98.0-8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/index.es.js CHANGED
@@ -47,7 +47,7 @@ const BOOK_LANGUAGE_VERSION = '1.0.0';
47
47
  * @generated
48
48
  * @see https://github.com/webgptorg/promptbook
49
49
  */
50
- const PROMPTBOOK_ENGINE_VERSION = '0.98.0-5';
50
+ const PROMPTBOOK_ENGINE_VERSION = '0.98.0-8';
51
51
  /**
52
52
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
53
53
  * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
@@ -261,7 +261,7 @@ const DEFAULT_MAX_PARALLEL_COUNT = 5; // <- TODO: [๐Ÿคนโ€โ™‚๏ธ]
261
261
  *
262
262
  * @public exported from `@promptbook/core`
263
263
  */
264
- const DEFAULT_MAX_EXECUTION_ATTEMPTS = 3; // <- TODO: [๐Ÿคนโ€โ™‚๏ธ]
264
+ const DEFAULT_MAX_EXECUTION_ATTEMPTS = 7; // <- TODO: [๐Ÿคนโ€โ™‚๏ธ]
265
265
  // <- TODO: [๐Ÿ]
266
266
  /**
267
267
  * Where to store your books
@@ -1060,7 +1060,7 @@ function jsonParse(value) {
1060
1060
  throw new Error(spaceTrim((block) => `
1061
1061
  ${block(error.message)}
1062
1062
 
1063
- The JSON text:
1063
+ The expected JSON text:
1064
1064
  ${block(value)}
1065
1065
  `));
1066
1066
  }
@@ -2758,6 +2758,361 @@ function extractParameterNames(template) {
2758
2758
  return parameterNames;
2759
2759
  }
2760
2760
 
2761
+ /**
2762
+ * Function isValidJsonString will tell you if the string is valid JSON or not
2763
+ *
2764
+ * @param value The string to check
2765
+ * @returns `true` if the string is a valid JSON string, false otherwise
2766
+ *
2767
+ * @public exported from `@promptbook/utils`
2768
+ */
2769
+ function isValidJsonString(value /* <- [๐Ÿ‘จโ€โš–๏ธ] */) {
2770
+ try {
2771
+ JSON.parse(value);
2772
+ return true;
2773
+ }
2774
+ catch (error) {
2775
+ assertsError(error);
2776
+ if (error.message.includes('Unexpected token')) {
2777
+ return false;
2778
+ }
2779
+ return false;
2780
+ }
2781
+ }
2782
+
2783
+ /**
2784
+ * Makes first letter of a string uppercase
2785
+ *
2786
+ * @public exported from `@promptbook/utils`
2787
+ */
2788
+ function capitalize(word) {
2789
+ return word.substring(0, 1).toUpperCase() + word.substring(1);
2790
+ }
2791
+
2792
+ /**
2793
+ * Extracts all code blocks from markdown.
2794
+ *
2795
+ * Note: There are multiple similar functions:
2796
+ * - `extractBlock` just extracts the content of the code block which is also used as built-in function for postprocessing
2797
+ * - `extractJsonBlock` extracts exactly one valid JSON code block
2798
+ * - `extractOneBlockFromMarkdown` extracts exactly one code block with language of the code block
2799
+ * - `extractAllBlocksFromMarkdown` extracts all code blocks with language of the code block
2800
+ *
2801
+ * @param markdown any valid markdown
2802
+ * @returns code blocks with language and content
2803
+ * @throws {ParseError} if block is not closed properly
2804
+ * @public exported from `@promptbook/markdown-utils`
2805
+ */
2806
+ function extractAllBlocksFromMarkdown(markdown) {
2807
+ const codeBlocks = [];
2808
+ const lines = markdown.split('\n');
2809
+ // Note: [0] Ensure that the last block notated by gt > will be closed
2810
+ lines.push('');
2811
+ let currentCodeBlock = null;
2812
+ for (const line of lines) {
2813
+ if (line.startsWith('> ') || line === '>') {
2814
+ if (currentCodeBlock === null) {
2815
+ currentCodeBlock = { blockNotation: '>', language: null, content: '' };
2816
+ } /* not else */
2817
+ if (currentCodeBlock.blockNotation === '>') {
2818
+ if (currentCodeBlock.content !== '') {
2819
+ currentCodeBlock.content += '\n';
2820
+ }
2821
+ currentCodeBlock.content += line.slice(2);
2822
+ }
2823
+ }
2824
+ else if (currentCodeBlock !== null && currentCodeBlock.blockNotation === '>' /* <- Note: [0] */) {
2825
+ codeBlocks.push(currentCodeBlock);
2826
+ currentCodeBlock = null;
2827
+ }
2828
+ /* not else */
2829
+ if (line.startsWith('```')) {
2830
+ const language = line.slice(3).trim() || null;
2831
+ if (currentCodeBlock === null) {
2832
+ currentCodeBlock = { blockNotation: '```', language, content: '' };
2833
+ }
2834
+ else {
2835
+ if (language !== null) {
2836
+ throw new ParseError(`${capitalize(currentCodeBlock.language || 'the')} code block was not closed and already opening new ${language} code block`);
2837
+ }
2838
+ codeBlocks.push(currentCodeBlock);
2839
+ currentCodeBlock = null;
2840
+ }
2841
+ }
2842
+ else if (currentCodeBlock !== null && currentCodeBlock.blockNotation === '```') {
2843
+ if (currentCodeBlock.content !== '') {
2844
+ currentCodeBlock.content += '\n';
2845
+ }
2846
+ currentCodeBlock.content += line.split('\\`\\`\\`').join('```') /* <- TODO: Maybe make proper unescape */;
2847
+ }
2848
+ }
2849
+ if (currentCodeBlock !== null) {
2850
+ throw new ParseError(`${capitalize(currentCodeBlock.language || 'the')} code block was not closed at the end of the markdown`);
2851
+ }
2852
+ return codeBlocks;
2853
+ }
2854
+ /**
2855
+ * TODO: Maybe name for `blockNotation` instead of '```' and '>'
2856
+ */
2857
+
2858
+ /**
2859
+ * Extracts extracts exactly one valid JSON code block
2860
+ *
2861
+ * - When given string is a valid JSON as it is, it just returns it
2862
+ * - When there is no JSON code block the function throws a `ParseError`
2863
+ * - When there are multiple JSON code blocks the function throws a `ParseError`
2864
+ *
2865
+ * Note: It is not important if marked as ```json BUT if it is VALID JSON
2866
+ * Note: There are multiple similar function:
2867
+ * - `extractBlock` just extracts the content of the code block which is also used as build-in function for postprocessing
2868
+ * - `extractJsonBlock` extracts exactly one valid JSON code block
2869
+ * - `extractOneBlockFromMarkdown` extracts exactly one code block with language of the code block
2870
+ * - `extractAllBlocksFromMarkdown` extracts all code blocks with language of the code block
2871
+ *
2872
+ * @public exported from `@promptbook/markdown-utils`
2873
+ * @throws {ParseError} if there is no valid JSON block in the markdown
2874
+ */
2875
+ function extractJsonBlock(markdown) {
2876
+ if (isValidJsonString(markdown)) {
2877
+ return markdown;
2878
+ }
2879
+ const codeBlocks = extractAllBlocksFromMarkdown(markdown);
2880
+ const jsonBlocks = codeBlocks.filter(({ content }) => isValidJsonString(content));
2881
+ if (jsonBlocks.length === 0) {
2882
+ throw new Error('There is no valid JSON block in the markdown');
2883
+ }
2884
+ if (jsonBlocks.length > 1) {
2885
+ throw new Error('There are multiple JSON code blocks in the markdown');
2886
+ }
2887
+ return jsonBlocks[0].content;
2888
+ }
2889
+ /**
2890
+ * TODO: Add some auto-healing logic + extract YAML, JSON5, TOML, etc.
2891
+ * TODO: [๐Ÿข] Make this logic part of `JsonFormatParser` or `isValidJsonString`
2892
+ */
2893
+
2894
+ /**
2895
+ * Counts number of characters in the text
2896
+ *
2897
+ * @public exported from `@promptbook/utils`
2898
+ */
2899
+ function countCharacters(text) {
2900
+ // Remove null characters
2901
+ text = text.replace(/\0/g, '');
2902
+ // Replace emojis (and also ZWJ sequence) with hyphens
2903
+ text = text.replace(/(\p{Extended_Pictographic})\p{Modifier_Symbol}/gu, '$1');
2904
+ text = text.replace(/(\p{Extended_Pictographic})[\u{FE00}-\u{FE0F}]/gu, '$1');
2905
+ text = text.replace(/\p{Extended_Pictographic}(\u{200D}\p{Extended_Pictographic})*/gu, '-');
2906
+ return text.length;
2907
+ }
2908
+ /**
2909
+ * TODO: [๐Ÿฅด] Implement counting in formats - like JSON, CSV, XML,...
2910
+ */
2911
+
2912
+ /**
2913
+ * Number of characters per standard line with 11pt Arial font size.
2914
+ *
2915
+ * @public exported from `@promptbook/utils`
2916
+ */
2917
+ const CHARACTERS_PER_STANDARD_LINE = 63;
2918
+ /**
2919
+ * Number of lines per standard A4 page with 11pt Arial font size and standard margins and spacing.
2920
+ *
2921
+ * @public exported from `@promptbook/utils`
2922
+ */
2923
+ const LINES_PER_STANDARD_PAGE = 44;
2924
+ /**
2925
+ * TODO: [๐Ÿง ] Should be this `constants.ts` or `config.ts`?
2926
+ * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
2927
+ */
2928
+
2929
+ /**
2930
+ * Counts number of lines in the text
2931
+ *
2932
+ * Note: This does not check only for the presence of newlines, but also for the length of the standard line.
2933
+ *
2934
+ * @public exported from `@promptbook/utils`
2935
+ */
2936
+ function countLines(text) {
2937
+ text = text.replace('\r\n', '\n');
2938
+ text = text.replace('\r', '\n');
2939
+ const lines = text.split('\n');
2940
+ return lines.reduce((count, line) => count + Math.ceil(line.length / CHARACTERS_PER_STANDARD_LINE), 0);
2941
+ }
2942
+ /**
2943
+ * TODO: [๐Ÿฅด] Implement counting in formats - like JSON, CSV, XML,...
2944
+ */
2945
+
2946
+ /**
2947
+ * Counts number of pages in the text
2948
+ *
2949
+ * Note: This does not check only for the count of newlines, but also for the length of the standard line and length of the standard page.
2950
+ *
2951
+ * @public exported from `@promptbook/utils`
2952
+ */
2953
+ function countPages(text) {
2954
+ return Math.ceil(countLines(text) / LINES_PER_STANDARD_PAGE);
2955
+ }
2956
+ /**
2957
+ * TODO: [๐Ÿฅด] Implement counting in formats - like JSON, CSV, XML,...
2958
+ */
2959
+
2960
+ /**
2961
+ * Counts number of paragraphs in the text
2962
+ *
2963
+ * @public exported from `@promptbook/utils`
2964
+ */
2965
+ function countParagraphs(text) {
2966
+ return text.split(/\n\s*\n/).filter((paragraph) => paragraph.trim() !== '').length;
2967
+ }
2968
+ /**
2969
+ * TODO: [๐Ÿฅด] Implement counting in formats - like JSON, CSV, XML,...
2970
+ */
2971
+
2972
+ /**
2973
+ * Split text into sentences
2974
+ *
2975
+ * @public exported from `@promptbook/utils`
2976
+ */
2977
+ function splitIntoSentences(text) {
2978
+ return text.split(/[.!?]+/).filter((sentence) => sentence.trim() !== '');
2979
+ }
2980
+ /**
2981
+ * Counts number of sentences in the text
2982
+ *
2983
+ * @public exported from `@promptbook/utils`
2984
+ */
2985
+ function countSentences(text) {
2986
+ return splitIntoSentences(text).length;
2987
+ }
2988
+ /**
2989
+ * TODO: [๐Ÿฅด] Implement counting in formats - like JSON, CSV, XML,...
2990
+ */
2991
+
2992
+ /**
2993
+ * Counts number of words in the text
2994
+ *
2995
+ * @public exported from `@promptbook/utils`
2996
+ */
2997
+ function countWords(text) {
2998
+ text = text.replace(/[\p{Extended_Pictographic}]/gu, 'a');
2999
+ text = removeDiacritics(text);
3000
+ // Add spaces before uppercase letters preceded by lowercase letters (for camelCase)
3001
+ text = text.replace(/([a-z])([A-Z])/g, '$1 $2');
3002
+ return text.split(/[^a-zะฐ-ั0-9]+/i).filter((word) => word.length > 0).length;
3003
+ }
3004
+ /**
3005
+ * TODO: [๐Ÿฅด] Implement counting in formats - like JSON, CSV, XML,...
3006
+ */
3007
+
3008
+ /**
3009
+ * Index of all counter functions
3010
+ *
3011
+ * @public exported from `@promptbook/utils`
3012
+ */
3013
+ const CountUtils = {
3014
+ CHARACTERS: countCharacters,
3015
+ WORDS: countWords,
3016
+ SENTENCES: countSentences,
3017
+ PARAGRAPHS: countParagraphs,
3018
+ LINES: countLines,
3019
+ PAGES: countPages,
3020
+ };
3021
+ /**
3022
+ * TODO: [๐Ÿง ][๐Ÿค ] This should be probably as part of `TextFormatParser`
3023
+ * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
3024
+ */
3025
+
3026
+ /**
3027
+ * Function checkExpectations will check if the expectations on given value are met
3028
+ *
3029
+ * Note: There are two similar functions:
3030
+ * - `checkExpectations` which throws an error if the expectations are not met
3031
+ * - `isPassingExpectations` which returns a boolean
3032
+ *
3033
+ * @throws {ExpectError} if the expectations are not met
3034
+ * @returns {void} Nothing
3035
+ * @private internal function of `createPipelineExecutor`
3036
+ */
3037
+ function checkExpectations(expectations, value) {
3038
+ for (const [unit, { max, min }] of Object.entries(expectations)) {
3039
+ const amount = CountUtils[unit.toUpperCase()](value);
3040
+ if (min && amount < min) {
3041
+ throw new ExpectError(`Expected at least ${min} ${unit} but got ${amount}`);
3042
+ } /* not else */
3043
+ if (max && amount > max) {
3044
+ throw new ExpectError(`Expected at most ${max} ${unit} but got ${amount}`);
3045
+ }
3046
+ }
3047
+ }
3048
+ /**
3049
+ * TODO: [๐Ÿ’] Unite object for expecting amount and format
3050
+ * TODO: [๐Ÿง ][๐Ÿค ] This should be part of `TextFormatParser`
3051
+ * Note: [๐Ÿ’] and [๐Ÿค ] are interconnected together
3052
+ */
3053
+
3054
+ /**
3055
+ * Validates a prompt result against expectations and format requirements.
3056
+ * This function provides a common abstraction for result validation that can be used
3057
+ * by both execution logic and caching logic to ensure consistency.
3058
+ *
3059
+ * @param options - The validation options including result string, expectations, and format
3060
+ * @returns Validation result with processed string and validity status
3061
+ * @private internal function of `createPipelineExecutor` and `cacheLlmTools`
3062
+ */
3063
+ function validatePromptResult(options) {
3064
+ const { resultString, expectations, format } = options;
3065
+ let processedResultString = resultString;
3066
+ let validationError;
3067
+ try {
3068
+ // TODO: [๐Ÿ’] Unite object for expecting amount and format
3069
+ if (format) {
3070
+ if (format === 'JSON') {
3071
+ if (!isValidJsonString(processedResultString)) {
3072
+ // TODO: [๐Ÿข] Do more universally via `FormatParser`
3073
+ try {
3074
+ processedResultString = extractJsonBlock(processedResultString);
3075
+ }
3076
+ catch (error) {
3077
+ keepUnused(error);
3078
+ throw new ExpectError(spaceTrim$1((block) => `
3079
+ Expected valid JSON string
3080
+
3081
+ The expected JSON text:
3082
+ ${block(processedResultString)}
3083
+ `));
3084
+ }
3085
+ }
3086
+ }
3087
+ else {
3088
+ throw new UnexpectedError(`Unknown format "${format}"`);
3089
+ }
3090
+ }
3091
+ // TODO: [๐Ÿ’] Unite object for expecting amount and format
3092
+ if (expectations) {
3093
+ checkExpectations(expectations, processedResultString);
3094
+ }
3095
+ return {
3096
+ isValid: true,
3097
+ processedResultString,
3098
+ };
3099
+ }
3100
+ catch (error) {
3101
+ if (error instanceof ExpectError) {
3102
+ validationError = error;
3103
+ }
3104
+ else {
3105
+ // Re-throw non-ExpectError errors (like UnexpectedError)
3106
+ throw error;
3107
+ }
3108
+ return {
3109
+ isValid: false,
3110
+ processedResultString,
3111
+ error: validationError,
3112
+ };
3113
+ }
3114
+ }
3115
+
2761
3116
  /**
2762
3117
  * Intercepts LLM tools and counts total usage of the tools
2763
3118
  *
@@ -2788,6 +3143,7 @@ function cacheLlmTools(llmTools, options = {}) {
2788
3143
  },
2789
3144
  };
2790
3145
  const callCommonModel = async (prompt) => {
3146
+ var _a;
2791
3147
  const { parameters, content, modelRequirements } = prompt;
2792
3148
  // <- Note: These are relevant things from the prompt that the cache key should depend on.
2793
3149
  // TODO: Maybe some standalone function for normalization of content for cache
@@ -2843,11 +3199,42 @@ function cacheLlmTools(llmTools, options = {}) {
2843
3199
  // 1. It has a content property that is null or undefined
2844
3200
  // 2. It has an error property that is truthy
2845
3201
  // 3. It has a success property that is explicitly false
2846
- const isFailedResult = promptResult.content === null ||
3202
+ // 4. It doesn't meet the prompt's expectations or format requirements
3203
+ const isBasicFailedResult = promptResult.content === null ||
2847
3204
  promptResult.content === undefined ||
2848
3205
  promptResult.error ||
2849
3206
  promptResult.success === false;
2850
- if (!isFailedResult) {
3207
+ let shouldCache = !isBasicFailedResult;
3208
+ // If the basic result is valid, check against expectations and format
3209
+ if (shouldCache && promptResult.content) {
3210
+ try {
3211
+ const validationResult = validatePromptResult({
3212
+ resultString: promptResult.content,
3213
+ expectations: prompt.expectations,
3214
+ format: prompt.format,
3215
+ });
3216
+ shouldCache = validationResult.isValid;
3217
+ if (!shouldCache && isVerbose) {
3218
+ console.info('Not caching result that fails expectations/format validation for key:', key, {
3219
+ content: promptResult.content,
3220
+ expectations: prompt.expectations,
3221
+ format: prompt.format,
3222
+ validationError: (_a = validationResult.error) === null || _a === void 0 ? void 0 : _a.message,
3223
+ });
3224
+ }
3225
+ }
3226
+ catch (error) {
3227
+ // If validation throws an unexpected error, don't cache
3228
+ shouldCache = false;
3229
+ if (isVerbose) {
3230
+ console.info('Not caching result due to validation error for key:', key, {
3231
+ content: promptResult.content,
3232
+ validationError: error instanceof Error ? error.message : String(error),
3233
+ });
3234
+ }
3235
+ }
3236
+ }
3237
+ if (shouldCache) {
2851
3238
  await storage.setItem(key, {
2852
3239
  date: $getCurrentDate(),
2853
3240
  promptbookVersion: PROMPTBOOK_ENGINE_VERSION,
@@ -2864,7 +3251,7 @@ function cacheLlmTools(llmTools, options = {}) {
2864
3251
  promptResult,
2865
3252
  });
2866
3253
  }
2867
- else if (isVerbose) {
3254
+ else if (isVerbose && isBasicFailedResult) {
2868
3255
  console.info('Not caching failed result for key:', key, {
2869
3256
  content: promptResult.content,
2870
3257
  error: promptResult.error,
@@ -4677,28 +5064,6 @@ async function loadArchive(filePath, fs) {
4677
5064
 
4678
5065
  var PipelineCollection = [{title:"Prepare Knowledge from Markdown",pipelineUrl:"https://promptbook.studio/promptbook/prepare-knowledge-from-markdown.book",formfactorName:"GENERIC",parameters:[{name:"knowledgeContent",description:"Markdown document content",isInput:true,isOutput:false},{name:"knowledgePieces",description:"The knowledge JSON object",isInput:false,isOutput:true}],tasks:[{taskType:"PROMPT_TASK",name:"knowledge",title:"Knowledge",content:"You are experienced data researcher, extract the important knowledge from the document.\n\n# Rules\n\n- Make pieces of information concise, clear, and easy to understand\n- One piece of information should be approximately 1 paragraph\n- Divide the paragraphs by markdown horizontal lines ---\n- Omit irrelevant information\n- Group redundant information\n- Write just extracted information, nothing else\n\n# The document\n\nTake information from this document:\n\n> {knowledgeContent}",resultingParameterName:"knowledgePieces",dependentParameterNames:["knowledgeContent"]}],personas:[],preparations:[],knowledgeSources:[],knowledgePieces:[],sources:[{type:"BOOK",path:null,content:"# Prepare Knowledge from Markdown\n\n- PIPELINE URL `https://promptbook.studio/promptbook/prepare-knowledge-from-markdown.book`\n- INPUT PARAMETER `{knowledgeContent}` Markdown document content\n- OUTPUT PARAMETER `{knowledgePieces}` The knowledge JSON object\n\n## Knowledge\n\n<!-- TODO: [๐Ÿ†] -FORMAT JSON -->\n\n```markdown\nYou are experienced data researcher, extract the important knowledge from the document.\n\n# Rules\n\n- Make pieces of information concise, clear, and easy to understand\n- One piece of information should be approximately 1 paragraph\n- Divide the paragraphs by markdown horizontal lines ---\n- Omit irrelevant information\n- Group redundant information\n- Write just extracted information, nothing else\n\n# The document\n\nTake information from this document:\n\n> {knowledgeContent}\n```\n\n`-> {knowledgePieces}`\n"}],sourceFile:"./books/prepare-knowledge-from-markdown.book"},{title:"Prepare Keywords",pipelineUrl:"https://promptbook.studio/promptbook/prepare-knowledge-keywords.book",formfactorName:"GENERIC",parameters:[{name:"knowledgePieceContent",description:"The content",isInput:true,isOutput:false},{name:"keywords",description:"Keywords separated by comma",isInput:false,isOutput:true}],tasks:[{taskType:"PROMPT_TASK",name:"knowledge",title:"Knowledge",content:"You are experienced data researcher, detect the important keywords in the document.\n\n# Rules\n\n- Write just keywords separated by comma\n\n# The document\n\nTake information from this document:\n\n> {knowledgePieceContent}",resultingParameterName:"keywords",dependentParameterNames:["knowledgePieceContent"]}],personas:[],preparations:[],knowledgeSources:[],knowledgePieces:[],sources:[{type:"BOOK",path:null,content:"# Prepare Keywords\n\n- PIPELINE URL `https://promptbook.studio/promptbook/prepare-knowledge-keywords.book`\n- INPUT PARAMETER `{knowledgePieceContent}` The content\n- OUTPUT PARAMETER `{keywords}` Keywords separated by comma\n\n## Knowledge\n\n<!-- TODO: [๐Ÿ†] -FORMAT JSON -->\n\n```markdown\nYou are experienced data researcher, detect the important keywords in the document.\n\n# Rules\n\n- Write just keywords separated by comma\n\n# The document\n\nTake information from this document:\n\n> {knowledgePieceContent}\n```\n\n`-> {keywords}`\n"}],sourceFile:"./books/prepare-knowledge-keywords.book"},{title:"Prepare Knowledge-piece Title",pipelineUrl:"https://promptbook.studio/promptbook/prepare-knowledge-title.book",formfactorName:"GENERIC",parameters:[{name:"knowledgePieceContent",description:"The content",isInput:true,isOutput:false},{name:"title",description:"The title of the document",isInput:false,isOutput:true}],tasks:[{taskType:"PROMPT_TASK",name:"knowledge",title:"Knowledge",content:"You are experienced content creator, write best title for the document.\n\n# Rules\n\n- Write just title, nothing else\n- Write maximum 5 words for the title\n\n# The document\n\n> {knowledgePieceContent}",resultingParameterName:"title",expectations:{words:{min:1,max:8}},dependentParameterNames:["knowledgePieceContent"]}],personas:[],preparations:[],knowledgeSources:[],knowledgePieces:[],sources:[{type:"BOOK",path:null,content:"# Prepare Knowledge-piece Title\n\n- PIPELINE URL `https://promptbook.studio/promptbook/prepare-knowledge-title.book`\n- INPUT PARAMETER `{knowledgePieceContent}` The content\n- OUTPUT PARAMETER `{title}` The title of the document\n\n## Knowledge\n\n- EXPECT MIN 1 WORD\n- EXPECT MAX 8 WORDS\n\n```markdown\nYou are experienced content creator, write best title for the document.\n\n# Rules\n\n- Write just title, nothing else\n- Write maximum 5 words for the title\n\n# The document\n\n> {knowledgePieceContent}\n```\n\n`-> {title}`\n"}],sourceFile:"./books/prepare-knowledge-title.book"},{title:"Prepare Persona",pipelineUrl:"https://promptbook.studio/promptbook/prepare-persona.book",formfactorName:"GENERIC",parameters:[{name:"availableModels",description:"List of available model names together with their descriptions as JSON",isInput:true,isOutput:false},{name:"personaDescription",description:"Description of the persona",isInput:true,isOutput:false},{name:"modelsRequirements",description:"Specific requirements for the model",isInput:false,isOutput:true}],tasks:[{taskType:"PROMPT_TASK",name:"make-model-requirements",title:"Make modelRequirements",content:"You are an experienced AI engineer, you need to find the best models for virtual assistants:\n\n## Example\n\n```json\n[\n {\n \"modelName\": \"gpt-4o\",\n \"systemMessage\": \"You are experienced AI engineer and helpful assistant.\",\n \"temperature\": 0.7\n },\n {\n \"modelName\": \"claude-3-5-sonnet\",\n \"systemMessage\": \"You are a friendly and knowledgeable chatbot.\",\n \"temperature\": 0.5\n }\n]\n```\n\n## Instructions\n\n- Your output format is JSON array\n- Sort best-fitting models first\n- Omit any models that are not suitable\n- Write just the JSON, no other text should be present\n- Array contain items with following keys:\n - `modelName`: The name of the model to use\n - `systemMessage`: The system message to provide context to the model\n - `temperature`: The sampling temperature to use\n\n### Key `modelName`\n\nHere are the available models:\n\n```json\n{availableModels}\n```\n\n### Key `systemMessage`\n\nThe system message is used to communicate instructions or provide context to the model at the beginning of a conversation. It is displayed in a different format compared to user messages, helping the model understand its role in the conversation. The system message typically guides the model's behavior, sets the tone, or specifies desired output from the model. By utilizing the system message effectively, users can steer the model towards generating more accurate and relevant responses.\n\nFor example:\n\n> You are an experienced AI engineer and helpful assistant.\n\n> You are a friendly and knowledgeable chatbot.\n\n### Key `temperature`\n\nThe sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit.\n\nYou can pick a value between 0 and 2. For example:\n\n- `0.1`: Low temperature, extremely conservative and deterministic\n- `0.5`: Medium temperature, balanced between conservative and creative\n- `1.0`: High temperature, creative and bit random\n- `1.5`: Very high temperature, extremely creative and often chaotic and unpredictable\n- `2.0`: Maximum temperature, completely random and unpredictable, for some extreme creative use cases\n\n# The assistant\n\nTake this description of the persona:\n\n> {personaDescription}",resultingParameterName:"modelsRequirements",format:"JSON",dependentParameterNames:["availableModels","personaDescription"]}],personas:[],preparations:[],knowledgeSources:[],knowledgePieces:[],sources:[{type:"BOOK",path:null,content:"# Prepare Persona\n\n- PIPELINE URL `https://promptbook.studio/promptbook/prepare-persona.book`\n- INPUT PARAMETER `{availableModels}` List of available model names together with their descriptions as JSON\n- INPUT PARAMETER `{personaDescription}` Description of the persona\n- OUTPUT PARAMETER `{modelsRequirements}` Specific requirements for the model\n\n## Make modelRequirements\n\n- FORMAT JSON\n\n```markdown\nYou are an experienced AI engineer, you need to find the best models for virtual assistants:\n\n## Example\n\n\\`\\`\\`json\n[\n {\n \"modelName\": \"gpt-4o\",\n \"systemMessage\": \"You are experienced AI engineer and helpful assistant.\",\n \"temperature\": 0.7\n },\n {\n \"modelName\": \"claude-3-5-sonnet\",\n \"systemMessage\": \"You are a friendly and knowledgeable chatbot.\",\n \"temperature\": 0.5\n }\n]\n\\`\\`\\`\n\n## Instructions\n\n- Your output format is JSON array\n- Sort best-fitting models first\n- Omit any models that are not suitable\n- Write just the JSON, no other text should be present\n- Array contain items with following keys:\n - `modelName`: The name of the model to use\n - `systemMessage`: The system message to provide context to the model\n - `temperature`: The sampling temperature to use\n\n### Key `modelName`\n\nHere are the available models:\n\n\\`\\`\\`json\n{availableModels}\n\\`\\`\\`\n\n### Key `systemMessage`\n\nThe system message is used to communicate instructions or provide context to the model at the beginning of a conversation. It is displayed in a different format compared to user messages, helping the model understand its role in the conversation. The system message typically guides the model's behavior, sets the tone, or specifies desired output from the model. By utilizing the system message effectively, users can steer the model towards generating more accurate and relevant responses.\n\nFor example:\n\n> You are an experienced AI engineer and helpful assistant.\n\n> You are a friendly and knowledgeable chatbot.\n\n### Key `temperature`\n\nThe sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit.\n\nYou can pick a value between 0 and 2. For example:\n\n- `0.1`: Low temperature, extremely conservative and deterministic\n- `0.5`: Medium temperature, balanced between conservative and creative\n- `1.0`: High temperature, creative and bit random\n- `1.5`: Very high temperature, extremely creative and often chaotic and unpredictable\n- `2.0`: Maximum temperature, completely random and unpredictable, for some extreme creative use cases\n\n# The assistant\n\nTake this description of the persona:\n\n> {personaDescription}\n```\n\n`-> {modelsRequirements}`\n"}],sourceFile:"./books/prepare-persona.book"},{title:"Prepare Title",pipelineUrl:"https://promptbook.studio/promptbook/prepare-title.book",formfactorName:"GENERIC",parameters:[{name:"book",description:"The book to prepare the title for",isInput:true,isOutput:false},{name:"title",description:"Best title for the book",isInput:false,isOutput:true}],tasks:[{taskType:"PROMPT_TASK",name:"make-title",title:"Make title",content:"Make best title for given text which describes the workflow:\n\n## Rules\n\n- Write just title, nothing else\n- Title should be concise and clear - Write maximum ideally 2 words, maximum 5 words\n- Title starts with emoticon\n- Title should not mention the input and output of the workflow but the main purpose of the workflow\n _For example, not \"โœ Convert Knowledge-piece to title\" but \"โœ Title\"_\n\n## The workflow\n\n> {book}",resultingParameterName:"title",expectations:{words:{min:1,max:8},lines:{min:1,max:1}},dependentParameterNames:["book"]}],personas:[],preparations:[],knowledgeSources:[],knowledgePieces:[],sources:[{type:"BOOK",path:null,content:"# Prepare Title\n\n- PIPELINE URL `https://promptbook.studio/promptbook/prepare-title.book`\n- INPUT PARAMETER `{book}` The book to prepare the title for\n- OUTPUT PARAMETER `{title}` Best title for the book\n\n## Make title\n\n- EXPECT MIN 1 Word\n- EXPECT MAX 8 Words\n- EXPECT EXACTLY 1 Line\n\n```markdown\nMake best title for given text which describes the workflow:\n\n## Rules\n\n- Write just title, nothing else\n- Title should be concise and clear - Write maximum ideally 2 words, maximum 5 words\n- Title starts with emoticon\n- Title should not mention the input and output of the workflow but the main purpose of the workflow\n _For example, not \"โœ Convert Knowledge-piece to title\" but \"โœ Title\"_\n\n## The workflow\n\n> {book}\n```\n\n`-> {title}`\n"}],sourceFile:"./books/prepare-title.book"}];
4679
5066
 
4680
- /**
4681
- * Function isValidJsonString will tell you if the string is valid JSON or not
4682
- *
4683
- * @param value The string to check
4684
- * @returns `true` if the string is a valid JSON string, false otherwise
4685
- *
4686
- * @public exported from `@promptbook/utils`
4687
- */
4688
- function isValidJsonString(value /* <- [๐Ÿ‘จโ€โš–๏ธ] */) {
4689
- try {
4690
- JSON.parse(value);
4691
- return true;
4692
- }
4693
- catch (error) {
4694
- assertsError(error);
4695
- if (error.message.includes('Unexpected token')) {
4696
- return false;
4697
- }
4698
- return false;
4699
- }
4700
- }
4701
-
4702
5067
  /**
4703
5068
  * Function `validatePipelineString` will validate the if the string is a valid pipeline string
4704
5069
  * It does not check if the string is fully logically correct, but if it is a string that can be a pipeline string or the string looks completely different.
@@ -4762,15 +5127,6 @@ function prettifyMarkdown(content) {
4762
5127
  }
4763
5128
  }
4764
5129
 
4765
- /**
4766
- * Makes first letter of a string uppercase
4767
- *
4768
- * @public exported from `@promptbook/utils`
4769
- */
4770
- function capitalize(word) {
4771
- return word.substring(0, 1).toUpperCase() + word.substring(1);
4772
- }
4773
-
4774
5130
  /**
4775
5131
  * Converts promptbook in JSON format to string format
4776
5132
  *
@@ -5682,142 +6038,41 @@ const CsvFormatParser = {
5682
6038
  ${block(csv.errors.map((error) => error.message).join('\n\n'))}
5683
6039
 
5684
6040
  The CSV setings:
5685
- ${block(JSON.stringify({ ...settings, ...MANDATORY_CSV_SETTINGS }, null, 2))}
5686
-
5687
- The CSV data:
5688
- ${block(value)}
5689
- `));
5690
- }
5691
- const mappedData = await Promise.all(csv.data.map(async (row, rowIndex) => {
5692
- return /* not await */ Promise.all(Object.entries(row).map(async ([key, value], columnIndex, array) => {
5693
- const index = rowIndex * Object.keys(row).length + columnIndex;
5694
- return /* not await */ mapCallback({ [key]: value }, index, array.length);
5695
- }));
5696
- }));
5697
- return unparse(mappedData, { ...settings, ...MANDATORY_CSV_SETTINGS });
5698
- },
5699
- },
5700
- ],
5701
- };
5702
- /**
5703
- * TODO: [๐Ÿ“] In `CsvFormatParser` implement simple `isValid`
5704
- * TODO: [๐Ÿ“] In `CsvFormatParser` implement partial `canBeValid`
5705
- * TODO: [๐Ÿ“] In `CsvFormatParser` implement `heal
5706
- * TODO: [๐Ÿ“] In `CsvFormatParser` implement `subvalueParsers`
5707
- * TODO: [๐Ÿข] Allow to expect something inside CSV objects and other formats
5708
- */
5709
-
5710
- /**
5711
- * Definition for JSON format
5712
- *
5713
- * @private still in development [๐Ÿข]
5714
- */
5715
- const JsonFormatParser = {
5716
- formatName: 'JSON',
5717
- mimeType: 'application/json',
5718
- isValid(value, settings, schema) {
5719
- return isValidJsonString(value);
5720
- },
5721
- canBeValid(partialValue, settings, schema) {
5722
- return true;
5723
- },
5724
- heal(value, settings, schema) {
5725
- throw new Error('Not implemented');
5726
- },
5727
- subvalueParsers: [],
5728
- };
5729
- /**
5730
- * TODO: [๐Ÿง ] Maybe proper instance of object
5731
- * TODO: [0] Make string_serialized_json
5732
- * TODO: [1] Make type for JSON Settings and Schema
5733
- * TODO: [๐Ÿง ] What to use for validating JSONs - JSON Schema, ZoD, typescript types/interfaces,...?
5734
- * TODO: [๐Ÿ“] In `JsonFormatParser` implement simple `isValid`
5735
- * TODO: [๐Ÿ“] In `JsonFormatParser` implement partial `canBeValid`
5736
- * TODO: [๐Ÿ“] In `JsonFormatParser` implement `heal
5737
- * TODO: [๐Ÿ“] In `JsonFormatParser` implement `subvalueParsers`
5738
- * TODO: [๐Ÿข] Allow to expect something inside JSON objects and other formats
5739
- */
5740
-
5741
- /**
5742
- * Definition for any text - this will be always valid
5743
- *
5744
- * Note: This is not useful for validation, but for splitting and mapping with `subvalueParsers`
5745
- *
5746
- * @public exported from `@promptbook/core`
5747
- */
5748
- const TextFormatParser = {
5749
- formatName: 'TEXT',
5750
- isValid(value) {
5751
- return typeof value === 'string';
5752
- },
5753
- canBeValid(partialValue) {
5754
- return typeof partialValue === 'string';
5755
- },
5756
- heal() {
5757
- throw new UnexpectedError('It does not make sense to call `TextFormatParser.heal`');
5758
- },
5759
- subvalueParsers: [
5760
- {
5761
- subvalueName: 'LINE',
5762
- async mapValues(options) {
5763
- const { value, mapCallback, onProgress } = options;
5764
- const lines = value.split('\n');
5765
- const mappedLines = await Promise.all(lines.map((lineContent, lineNumber, array) =>
5766
- // TODO: [๐Ÿง ] Maybe option to skip empty line
5767
- /* not await */ mapCallback({
5768
- lineContent,
5769
- // TODO: [๐Ÿง ] Maybe also put here `lineNumber`
5770
- }, lineNumber, array.length)));
5771
- return mappedLines.join('\n');
6041
+ ${block(JSON.stringify({ ...settings, ...MANDATORY_CSV_SETTINGS }, null, 2))}
6042
+
6043
+ The CSV data:
6044
+ ${block(value)}
6045
+ `));
6046
+ }
6047
+ const mappedData = await Promise.all(csv.data.map(async (row, rowIndex) => {
6048
+ return /* not await */ Promise.all(Object.entries(row).map(async ([key, value], columnIndex, array) => {
6049
+ const index = rowIndex * Object.keys(row).length + columnIndex;
6050
+ return /* not await */ mapCallback({ [key]: value }, index, array.length);
6051
+ }));
6052
+ }));
6053
+ return unparse(mappedData, { ...settings, ...MANDATORY_CSV_SETTINGS });
5772
6054
  },
5773
6055
  },
5774
- // <- TODO: [๐Ÿง ][๐Ÿค ] Here should be all words, characters, lines, paragraphs, pages available as subvalues
5775
6056
  ],
5776
6057
  };
5777
6058
  /**
5778
- * TODO: [1] Make type for XML Text and Schema
5779
- * TODO: [๐Ÿง ][๐Ÿค ] Here should be all words, characters, lines, paragraphs, pages available as subvalues
5780
- * TODO: [๐Ÿ“] In `TextFormatParser` implement simple `isValid`
5781
- * TODO: [๐Ÿ“] In `TextFormatParser` implement partial `canBeValid`
5782
- * TODO: [๐Ÿ“] In `TextFormatParser` implement `heal
5783
- * TODO: [๐Ÿ“] In `TextFormatParser` implement `subvalueParsers`
5784
- * TODO: [๐Ÿข] Allow to expect something inside each item of list and other formats
5785
- */
5786
-
5787
- /**
5788
- * Function to check if a string is valid XML
5789
- *
5790
- * @param value
5791
- * @returns `true` if the string is a valid XML string, false otherwise
5792
- *
5793
- * @public exported from `@promptbook/utils`
6059
+ * TODO: [๐Ÿ“] In `CsvFormatParser` implement simple `isValid`
6060
+ * TODO: [๐Ÿ“] In `CsvFormatParser` implement partial `canBeValid`
6061
+ * TODO: [๐Ÿ“] In `CsvFormatParser` implement `heal
6062
+ * TODO: [๐Ÿ“] In `CsvFormatParser` implement `subvalueParsers`
6063
+ * TODO: [๐Ÿข] Allow to expect something inside CSV objects and other formats
5794
6064
  */
5795
- function isValidXmlString(value) {
5796
- try {
5797
- const parser = new DOMParser();
5798
- const parsedDocument = parser.parseFromString(value, 'application/xml');
5799
- const parserError = parsedDocument.getElementsByTagName('parsererror');
5800
- if (parserError.length > 0) {
5801
- return false;
5802
- }
5803
- return true;
5804
- }
5805
- catch (error) {
5806
- assertsError(error);
5807
- return false;
5808
- }
5809
- }
5810
6065
 
5811
6066
  /**
5812
- * Definition for XML format
6067
+ * Definition for JSON format
5813
6068
  *
5814
6069
  * @private still in development [๐Ÿข]
5815
6070
  */
5816
- const XmlFormatParser = {
5817
- formatName: 'XML',
5818
- mimeType: 'application/xml',
6071
+ const JsonFormatParser = {
6072
+ formatName: 'JSON',
6073
+ mimeType: 'application/json',
5819
6074
  isValid(value, settings, schema) {
5820
- return isValidXmlString(value);
6075
+ return isValidJsonString(value);
5821
6076
  },
5822
6077
  canBeValid(partialValue, settings, schema) {
5823
6078
  return true;
@@ -5829,443 +6084,282 @@ const XmlFormatParser = {
5829
6084
  };
5830
6085
  /**
5831
6086
  * TODO: [๐Ÿง ] Maybe proper instance of object
5832
- * TODO: [0] Make string_serialized_xml
5833
- * TODO: [1] Make type for XML Settings and Schema
5834
- * TODO: [๐Ÿง ] What to use for validating XMLs - XSD,...
5835
- * TODO: [๐Ÿ“] In `XmlFormatParser` implement simple `isValid`
5836
- * TODO: [๐Ÿ“] In `XmlFormatParser` implement partial `canBeValid`
5837
- * TODO: [๐Ÿ“] In `XmlFormatParser` implement `heal
5838
- * TODO: [๐Ÿ“] In `XmlFormatParser` implement `subvalueParsers`
5839
- * TODO: [๐Ÿข] Allow to expect something inside XML and other formats
5840
- */
5841
-
5842
- /**
5843
- * Definitions for all formats supported by Promptbook
5844
- *
5845
- * @private internal index of `...` <- TODO [๐Ÿข]
5846
- */
5847
- const FORMAT_DEFINITIONS = [JsonFormatParser, XmlFormatParser, TextFormatParser, CsvFormatParser];
5848
- /**
5849
- * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
5850
- */
5851
-
5852
- /**
5853
- * Maps available parameters to expected parameters for a pipeline task.
5854
- *
5855
- * The strategy is:
5856
- * 1) First, match parameters by name where both available and expected.
5857
- * 2) Then, if there are unmatched expected and available parameters, map them by order.
5858
- *
5859
- * @throws {PipelineExecutionError} If the number of unmatched expected and available parameters does not match, or mapping is ambiguous.
5860
- * @private within the repository used in `createPipelineExecutor`
5861
- */
5862
- function mapAvailableToExpectedParameters(options) {
5863
- const { expectedParameters, availableParameters } = options;
5864
- const availableParametersNames = new Set(Object.keys(availableParameters));
5865
- const expectedParameterNames = new Set(Object.keys(expectedParameters));
5866
- const mappedParameters = {};
5867
- // Phase 1๏ธโƒฃ: Matching mapping
5868
- for (const parameterName of Array.from(union(availableParametersNames, expectedParameterNames))) {
5869
- // Situation: Parameter is available and expected
5870
- if (availableParametersNames.has(parameterName) && expectedParameterNames.has(parameterName)) {
5871
- mappedParameters[parameterName] = availableParameters[parameterName];
5872
- // <- Note: [๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ง] Maybe detect parameter collision here?
5873
- availableParametersNames.delete(parameterName);
5874
- expectedParameterNames.delete(parameterName);
5875
- }
5876
- // Situation: Parameter is available but NOT expected
5877
- else if (availableParametersNames.has(parameterName) && !expectedParameterNames.has(parameterName)) ;
5878
- // Situation: Parameter is NOT available BUT expected
5879
- else if (!availableParametersNames.has(parameterName) && expectedParameterNames.has(parameterName)) ;
5880
- }
5881
- if (expectedParameterNames.size === 0) {
5882
- // Note: [๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ง] Now we can freeze `mappedParameters` to prevent accidental modifications after mapping
5883
- Object.freeze(mappedParameters);
5884
- return mappedParameters;
5885
- }
5886
- // Phase 2๏ธโƒฃ: Non-matching mapping
5887
- if (expectedParameterNames.size !== availableParametersNames.size) {
5888
- throw new PipelineExecutionError(spaceTrim((block) => `
5889
- Can not map available parameters to expected parameters
5890
-
5891
- Mapped parameters:
5892
- ${block(Object.keys(mappedParameters)
5893
- .map((parameterName) => `- {${parameterName}}`)
5894
- .join('\n'))}
5895
-
5896
- Expected parameters which can not be mapped:
5897
- ${block(Array.from(expectedParameterNames)
5898
- .map((parameterName) => `- {${parameterName}}`)
5899
- .join('\n'))}
5900
-
5901
- Remaining available parameters:
5902
- ${block(Array.from(availableParametersNames)
5903
- .map((parameterName) => `- {${parameterName}}`)
5904
- .join('\n'))}
5905
-
5906
- `));
5907
- }
5908
- const expectedParameterNamesArray = Array.from(expectedParameterNames);
5909
- const availableParametersNamesArray = Array.from(availableParametersNames);
5910
- for (let i = 0; i < expectedParameterNames.size; i++) {
5911
- mappedParameters[expectedParameterNamesArray[i]] = availableParameters[availableParametersNamesArray[i]];
5912
- }
5913
- // Note: [๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ง] Now we can freeze `mappedParameters` to prevent accidental modifications after mapping
5914
- Object.freeze(mappedParameters);
5915
- return mappedParameters;
5916
- }
5917
-
5918
- /**
5919
- * Extracts all code blocks from markdown.
5920
- *
5921
- * Note: There are multiple similar functions:
5922
- * - `extractBlock` just extracts the content of the code block which is also used as built-in function for postprocessing
5923
- * - `extractJsonBlock` extracts exactly one valid JSON code block
5924
- * - `extractOneBlockFromMarkdown` extracts exactly one code block with language of the code block
5925
- * - `extractAllBlocksFromMarkdown` extracts all code blocks with language of the code block
5926
- *
5927
- * @param markdown any valid markdown
5928
- * @returns code blocks with language and content
5929
- * @throws {ParseError} if block is not closed properly
5930
- * @public exported from `@promptbook/markdown-utils`
5931
- */
5932
- function extractAllBlocksFromMarkdown(markdown) {
5933
- const codeBlocks = [];
5934
- const lines = markdown.split('\n');
5935
- // Note: [0] Ensure that the last block notated by gt > will be closed
5936
- lines.push('');
5937
- let currentCodeBlock = null;
5938
- for (const line of lines) {
5939
- if (line.startsWith('> ') || line === '>') {
5940
- if (currentCodeBlock === null) {
5941
- currentCodeBlock = { blockNotation: '>', language: null, content: '' };
5942
- } /* not else */
5943
- if (currentCodeBlock.blockNotation === '>') {
5944
- if (currentCodeBlock.content !== '') {
5945
- currentCodeBlock.content += '\n';
5946
- }
5947
- currentCodeBlock.content += line.slice(2);
5948
- }
5949
- }
5950
- else if (currentCodeBlock !== null && currentCodeBlock.blockNotation === '>' /* <- Note: [0] */) {
5951
- codeBlocks.push(currentCodeBlock);
5952
- currentCodeBlock = null;
5953
- }
5954
- /* not else */
5955
- if (line.startsWith('```')) {
5956
- const language = line.slice(3).trim() || null;
5957
- if (currentCodeBlock === null) {
5958
- currentCodeBlock = { blockNotation: '```', language, content: '' };
5959
- }
5960
- else {
5961
- if (language !== null) {
5962
- throw new ParseError(`${capitalize(currentCodeBlock.language || 'the')} code block was not closed and already opening new ${language} code block`);
5963
- }
5964
- codeBlocks.push(currentCodeBlock);
5965
- currentCodeBlock = null;
5966
- }
5967
- }
5968
- else if (currentCodeBlock !== null && currentCodeBlock.blockNotation === '```') {
5969
- if (currentCodeBlock.content !== '') {
5970
- currentCodeBlock.content += '\n';
5971
- }
5972
- currentCodeBlock.content += line.split('\\`\\`\\`').join('```') /* <- TODO: Maybe make proper unescape */;
5973
- }
5974
- }
5975
- if (currentCodeBlock !== null) {
5976
- throw new ParseError(`${capitalize(currentCodeBlock.language || 'the')} code block was not closed at the end of the markdown`);
5977
- }
5978
- return codeBlocks;
5979
- }
5980
- /**
5981
- * TODO: Maybe name for `blockNotation` instead of '```' and '>'
5982
- */
5983
-
5984
- /**
5985
- * Extracts extracts exactly one valid JSON code block
5986
- *
5987
- * - When given string is a valid JSON as it is, it just returns it
5988
- * - When there is no JSON code block the function throws a `ParseError`
5989
- * - When there are multiple JSON code blocks the function throws a `ParseError`
5990
- *
5991
- * Note: It is not important if marked as ```json BUT if it is VALID JSON
5992
- * Note: There are multiple similar function:
5993
- * - `extractBlock` just extracts the content of the code block which is also used as build-in function for postprocessing
5994
- * - `extractJsonBlock` extracts exactly one valid JSON code block
5995
- * - `extractOneBlockFromMarkdown` extracts exactly one code block with language of the code block
5996
- * - `extractAllBlocksFromMarkdown` extracts all code blocks with language of the code block
5997
- *
5998
- * @public exported from `@promptbook/markdown-utils`
5999
- * @throws {ParseError} if there is no valid JSON block in the markdown
6000
- */
6001
- function extractJsonBlock(markdown) {
6002
- if (isValidJsonString(markdown)) {
6003
- return markdown;
6004
- }
6005
- const codeBlocks = extractAllBlocksFromMarkdown(markdown);
6006
- const jsonBlocks = codeBlocks.filter(({ content }) => isValidJsonString(content));
6007
- if (jsonBlocks.length === 0) {
6008
- throw new Error('There is no valid JSON block in the markdown');
6009
- }
6010
- if (jsonBlocks.length > 1) {
6011
- throw new Error('There are multiple JSON code blocks in the markdown');
6012
- }
6013
- return jsonBlocks[0].content;
6014
- }
6015
- /**
6016
- * TODO: Add some auto-healing logic + extract YAML, JSON5, TOML, etc.
6017
- * TODO: [๐Ÿข] Make this logic part of `JsonFormatParser` or `isValidJsonString`
6087
+ * TODO: [0] Make string_serialized_json
6088
+ * TODO: [1] Make type for JSON Settings and Schema
6089
+ * TODO: [๐Ÿง ] What to use for validating JSONs - JSON Schema, ZoD, typescript types/interfaces,...?
6090
+ * TODO: [๐Ÿ“] In `JsonFormatParser` implement simple `isValid`
6091
+ * TODO: [๐Ÿ“] In `JsonFormatParser` implement partial `canBeValid`
6092
+ * TODO: [๐Ÿ“] In `JsonFormatParser` implement `heal
6093
+ * TODO: [๐Ÿ“] In `JsonFormatParser` implement `subvalueParsers`
6094
+ * TODO: [๐Ÿข] Allow to expect something inside JSON objects and other formats
6018
6095
  */
6019
6096
 
6020
6097
  /**
6021
- * Takes an item or an array of items and returns an array of items
6098
+ * Definition for any text - this will be always valid
6022
6099
  *
6023
- * 1) Any item except array and undefined returns array with that one item (also null)
6024
- * 2) Undefined returns empty array
6025
- * 3) Array returns itself
6100
+ * Note: This is not useful for validation, but for splitting and mapping with `subvalueParsers`
6026
6101
  *
6027
- * @private internal utility
6102
+ * @public exported from `@promptbook/core`
6103
+ */
6104
+ const TextFormatParser = {
6105
+ formatName: 'TEXT',
6106
+ isValid(value) {
6107
+ return typeof value === 'string';
6108
+ },
6109
+ canBeValid(partialValue) {
6110
+ return typeof partialValue === 'string';
6111
+ },
6112
+ heal() {
6113
+ throw new UnexpectedError('It does not make sense to call `TextFormatParser.heal`');
6114
+ },
6115
+ subvalueParsers: [
6116
+ {
6117
+ subvalueName: 'LINE',
6118
+ async mapValues(options) {
6119
+ const { value, mapCallback, onProgress } = options;
6120
+ const lines = value.split('\n');
6121
+ const mappedLines = await Promise.all(lines.map((lineContent, lineNumber, array) =>
6122
+ // TODO: [๐Ÿง ] Maybe option to skip empty line
6123
+ /* not await */ mapCallback({
6124
+ lineContent,
6125
+ // TODO: [๐Ÿง ] Maybe also put here `lineNumber`
6126
+ }, lineNumber, array.length)));
6127
+ return mappedLines.join('\n');
6128
+ },
6129
+ },
6130
+ // <- TODO: [๐Ÿง ][๐Ÿค ] Here should be all words, characters, lines, paragraphs, pages available as subvalues
6131
+ ],
6132
+ };
6133
+ /**
6134
+ * TODO: [1] Make type for XML Text and Schema
6135
+ * TODO: [๐Ÿง ][๐Ÿค ] Here should be all words, characters, lines, paragraphs, pages available as subvalues
6136
+ * TODO: [๐Ÿ“] In `TextFormatParser` implement simple `isValid`
6137
+ * TODO: [๐Ÿ“] In `TextFormatParser` implement partial `canBeValid`
6138
+ * TODO: [๐Ÿ“] In `TextFormatParser` implement `heal
6139
+ * TODO: [๐Ÿ“] In `TextFormatParser` implement `subvalueParsers`
6140
+ * TODO: [๐Ÿข] Allow to expect something inside each item of list and other formats
6028
6141
  */
6029
- function arrayableToArray(input) {
6030
- if (input === undefined) {
6031
- return [];
6032
- }
6033
- if (input instanceof Array) {
6034
- return input;
6035
- }
6036
- return [input];
6037
- }
6038
6142
 
6039
6143
  /**
6040
- * Replaces parameters in template with values from parameters object
6144
+ * Function to check if a string is valid XML
6041
6145
  *
6042
- * Note: This function is not places strings into string,
6043
- * It's more complex and can handle this operation specifically for LLM models
6146
+ * @param value
6147
+ * @returns `true` if the string is a valid XML string, false otherwise
6044
6148
  *
6045
- * @param template the template with parameters in {curly} braces
6046
- * @param parameters the object with parameters
6047
- * @returns the template with replaced parameters
6048
- * @throws {PipelineExecutionError} if parameter is not defined, not closed, or not opened
6049
6149
  * @public exported from `@promptbook/utils`
6050
6150
  */
6051
- function templateParameters(template, parameters) {
6052
- for (const [parameterName, parameterValue] of Object.entries(parameters)) {
6053
- if (parameterValue === RESERVED_PARAMETER_MISSING_VALUE) {
6054
- throw new UnexpectedError(`Parameter \`{${parameterName}}\` has missing value`);
6055
- }
6056
- else if (parameterValue === RESERVED_PARAMETER_RESTRICTED) {
6057
- // TODO: [๐Ÿต]
6058
- throw new UnexpectedError(`Parameter \`{${parameterName}}\` is restricted to use`);
6059
- }
6060
- }
6061
- let replacedTemplates = template;
6062
- let match;
6063
- let loopLimit = LOOP_LIMIT;
6064
- while ((match = /^(?<precol>.*){(?<parameterName>\w+)}(.*)/m /* <- Not global */
6065
- .exec(replacedTemplates))) {
6066
- if (loopLimit-- < 0) {
6067
- throw new LimitReachedError('Loop limit reached during parameters replacement in `templateParameters`');
6068
- }
6069
- const precol = match.groups.precol;
6070
- const parameterName = match.groups.parameterName;
6071
- if (parameterName === '') {
6072
- // Note: Skip empty placeholders. It's used to avoid confusion with JSON-like strings
6073
- continue;
6074
- }
6075
- if (parameterName.indexOf('{') !== -1 || parameterName.indexOf('}') !== -1) {
6076
- throw new PipelineExecutionError('Parameter is already opened or not closed');
6077
- }
6078
- if (parameters[parameterName] === undefined) {
6079
- throw new PipelineExecutionError(`Parameter \`{${parameterName}}\` is not defined`);
6080
- }
6081
- let parameterValue = parameters[parameterName];
6082
- if (parameterValue === undefined) {
6083
- throw new PipelineExecutionError(`Parameter \`{${parameterName}}\` is not defined`);
6084
- }
6085
- parameterValue = valueToString(parameterValue);
6086
- // Escape curly braces in parameter values to prevent prompt-injection
6087
- parameterValue = parameterValue.replace(/[{}]/g, '\\$&');
6088
- if (parameterValue.includes('\n') && /^\s*\W{0,3}\s*$/.test(precol)) {
6089
- parameterValue = parameterValue
6090
- .split('\n')
6091
- .map((line, index) => (index === 0 ? line : `${precol}${line}`))
6092
- .join('\n');
6151
+ function isValidXmlString(value) {
6152
+ try {
6153
+ const parser = new DOMParser();
6154
+ const parsedDocument = parser.parseFromString(value, 'application/xml');
6155
+ const parserError = parsedDocument.getElementsByTagName('parsererror');
6156
+ if (parserError.length > 0) {
6157
+ return false;
6093
6158
  }
6094
- replacedTemplates =
6095
- replacedTemplates.substring(0, match.index + precol.length) +
6096
- parameterValue +
6097
- replacedTemplates.substring(match.index + precol.length + parameterName.length + 2);
6098
- }
6099
- // [๐Ÿ’ซ] Check if there are parameters that are not closed properly
6100
- if (/{\w+$/.test(replacedTemplates)) {
6101
- throw new PipelineExecutionError('Parameter is not closed');
6159
+ return true;
6102
6160
  }
6103
- // [๐Ÿ’ซ] Check if there are parameters that are not opened properly
6104
- if (/^\w+}/.test(replacedTemplates)) {
6105
- throw new PipelineExecutionError('Parameter is not opened');
6161
+ catch (error) {
6162
+ assertsError(error);
6163
+ return false;
6106
6164
  }
6107
- return replacedTemplates;
6108
6165
  }
6109
6166
 
6110
6167
  /**
6111
- * Counts number of characters in the text
6168
+ * Definition for XML format
6112
6169
  *
6113
- * @public exported from `@promptbook/utils`
6170
+ * @private still in development [๐Ÿข]
6114
6171
  */
6115
- function countCharacters(text) {
6116
- // Remove null characters
6117
- text = text.replace(/\0/g, '');
6118
- // Replace emojis (and also ZWJ sequence) with hyphens
6119
- text = text.replace(/(\p{Extended_Pictographic})\p{Modifier_Symbol}/gu, '$1');
6120
- text = text.replace(/(\p{Extended_Pictographic})[\u{FE00}-\u{FE0F}]/gu, '$1');
6121
- text = text.replace(/\p{Extended_Pictographic}(\u{200D}\p{Extended_Pictographic})*/gu, '-');
6122
- return text.length;
6123
- }
6172
+ const XmlFormatParser = {
6173
+ formatName: 'XML',
6174
+ mimeType: 'application/xml',
6175
+ isValid(value, settings, schema) {
6176
+ return isValidXmlString(value);
6177
+ },
6178
+ canBeValid(partialValue, settings, schema) {
6179
+ return true;
6180
+ },
6181
+ heal(value, settings, schema) {
6182
+ throw new Error('Not implemented');
6183
+ },
6184
+ subvalueParsers: [],
6185
+ };
6124
6186
  /**
6125
- * TODO: [๐Ÿฅด] Implement counting in formats - like JSON, CSV, XML,...
6187
+ * TODO: [๐Ÿง ] Maybe proper instance of object
6188
+ * TODO: [0] Make string_serialized_xml
6189
+ * TODO: [1] Make type for XML Settings and Schema
6190
+ * TODO: [๐Ÿง ] What to use for validating XMLs - XSD,...
6191
+ * TODO: [๐Ÿ“] In `XmlFormatParser` implement simple `isValid`
6192
+ * TODO: [๐Ÿ“] In `XmlFormatParser` implement partial `canBeValid`
6193
+ * TODO: [๐Ÿ“] In `XmlFormatParser` implement `heal
6194
+ * TODO: [๐Ÿ“] In `XmlFormatParser` implement `subvalueParsers`
6195
+ * TODO: [๐Ÿข] Allow to expect something inside XML and other formats
6126
6196
  */
6127
6197
 
6128
6198
  /**
6129
- * Number of characters per standard line with 11pt Arial font size.
6130
- *
6131
- * @public exported from `@promptbook/utils`
6132
- */
6133
- const CHARACTERS_PER_STANDARD_LINE = 63;
6134
- /**
6135
- * Number of lines per standard A4 page with 11pt Arial font size and standard margins and spacing.
6199
+ * Definitions for all formats supported by Promptbook
6136
6200
  *
6137
- * @public exported from `@promptbook/utils`
6201
+ * @private internal index of `...` <- TODO [๐Ÿข]
6138
6202
  */
6139
- const LINES_PER_STANDARD_PAGE = 44;
6203
+ const FORMAT_DEFINITIONS = [JsonFormatParser, XmlFormatParser, TextFormatParser, CsvFormatParser];
6140
6204
  /**
6141
- * TODO: [๐Ÿง ] Should be this `constants.ts` or `config.ts`?
6142
6205
  * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
6143
6206
  */
6144
6207
 
6145
6208
  /**
6146
- * Counts number of lines in the text
6209
+ * Maps available parameters to expected parameters for a pipeline task.
6147
6210
  *
6148
- * Note: This does not check only for the presence of newlines, but also for the length of the standard line.
6211
+ * The strategy is:
6212
+ * 1) First, match parameters by name where both available and expected.
6213
+ * 2) Then, if there are unmatched expected and available parameters, map them by order.
6149
6214
  *
6150
- * @public exported from `@promptbook/utils`
6151
- */
6152
- function countLines(text) {
6153
- text = text.replace('\r\n', '\n');
6154
- text = text.replace('\r', '\n');
6155
- const lines = text.split('\n');
6156
- return lines.reduce((count, line) => count + Math.ceil(line.length / CHARACTERS_PER_STANDARD_LINE), 0);
6157
- }
6158
- /**
6159
- * TODO: [๐Ÿฅด] Implement counting in formats - like JSON, CSV, XML,...
6215
+ * @throws {PipelineExecutionError} If the number of unmatched expected and available parameters does not match, or mapping is ambiguous.
6216
+ * @private within the repository used in `createPipelineExecutor`
6160
6217
  */
6218
+ function mapAvailableToExpectedParameters(options) {
6219
+ const { expectedParameters, availableParameters } = options;
6220
+ const availableParametersNames = new Set(Object.keys(availableParameters));
6221
+ const expectedParameterNames = new Set(Object.keys(expectedParameters));
6222
+ const mappedParameters = {};
6223
+ // Phase 1๏ธโƒฃ: Matching mapping
6224
+ for (const parameterName of Array.from(union(availableParametersNames, expectedParameterNames))) {
6225
+ // Situation: Parameter is available and expected
6226
+ if (availableParametersNames.has(parameterName) && expectedParameterNames.has(parameterName)) {
6227
+ mappedParameters[parameterName] = availableParameters[parameterName];
6228
+ // <- Note: [๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ง] Maybe detect parameter collision here?
6229
+ availableParametersNames.delete(parameterName);
6230
+ expectedParameterNames.delete(parameterName);
6231
+ }
6232
+ // Situation: Parameter is available but NOT expected
6233
+ else if (availableParametersNames.has(parameterName) && !expectedParameterNames.has(parameterName)) ;
6234
+ // Situation: Parameter is NOT available BUT expected
6235
+ else if (!availableParametersNames.has(parameterName) && expectedParameterNames.has(parameterName)) ;
6236
+ }
6237
+ if (expectedParameterNames.size === 0) {
6238
+ // Note: [๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ง] Now we can freeze `mappedParameters` to prevent accidental modifications after mapping
6239
+ Object.freeze(mappedParameters);
6240
+ return mappedParameters;
6241
+ }
6242
+ // Phase 2๏ธโƒฃ: Non-matching mapping
6243
+ if (expectedParameterNames.size !== availableParametersNames.size) {
6244
+ throw new PipelineExecutionError(spaceTrim((block) => `
6245
+ Can not map available parameters to expected parameters
6161
6246
 
6162
- /**
6163
- * Counts number of pages in the text
6164
- *
6165
- * Note: This does not check only for the count of newlines, but also for the length of the standard line and length of the standard page.
6166
- *
6167
- * @public exported from `@promptbook/utils`
6168
- */
6169
- function countPages(text) {
6170
- return Math.ceil(countLines(text) / LINES_PER_STANDARD_PAGE);
6171
- }
6172
- /**
6173
- * TODO: [๐Ÿฅด] Implement counting in formats - like JSON, CSV, XML,...
6174
- */
6247
+ Mapped parameters:
6248
+ ${block(Object.keys(mappedParameters)
6249
+ .map((parameterName) => `- {${parameterName}}`)
6250
+ .join('\n'))}
6175
6251
 
6176
- /**
6177
- * Counts number of paragraphs in the text
6178
- *
6179
- * @public exported from `@promptbook/utils`
6180
- */
6181
- function countParagraphs(text) {
6182
- return text.split(/\n\s*\n/).filter((paragraph) => paragraph.trim() !== '').length;
6252
+ Expected parameters which can not be mapped:
6253
+ ${block(Array.from(expectedParameterNames)
6254
+ .map((parameterName) => `- {${parameterName}}`)
6255
+ .join('\n'))}
6256
+
6257
+ Remaining available parameters:
6258
+ ${block(Array.from(availableParametersNames)
6259
+ .map((parameterName) => `- {${parameterName}}`)
6260
+ .join('\n'))}
6261
+
6262
+ `));
6263
+ }
6264
+ const expectedParameterNamesArray = Array.from(expectedParameterNames);
6265
+ const availableParametersNamesArray = Array.from(availableParametersNames);
6266
+ for (let i = 0; i < expectedParameterNames.size; i++) {
6267
+ mappedParameters[expectedParameterNamesArray[i]] = availableParameters[availableParametersNamesArray[i]];
6268
+ }
6269
+ // Note: [๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ง] Now we can freeze `mappedParameters` to prevent accidental modifications after mapping
6270
+ Object.freeze(mappedParameters);
6271
+ return mappedParameters;
6183
6272
  }
6184
- /**
6185
- * TODO: [๐Ÿฅด] Implement counting in formats - like JSON, CSV, XML,...
6186
- */
6187
6273
 
6188
6274
  /**
6189
- * Split text into sentences
6275
+ * Takes an item or an array of items and returns an array of items
6190
6276
  *
6191
- * @public exported from `@promptbook/utils`
6192
- */
6193
- function splitIntoSentences(text) {
6194
- return text.split(/[.!?]+/).filter((sentence) => sentence.trim() !== '');
6195
- }
6196
- /**
6197
- * Counts number of sentences in the text
6277
+ * 1) Any item except array and undefined returns array with that one item (also null)
6278
+ * 2) Undefined returns empty array
6279
+ * 3) Array returns itself
6198
6280
  *
6199
- * @public exported from `@promptbook/utils`
6281
+ * @private internal utility
6200
6282
  */
6201
- function countSentences(text) {
6202
- return splitIntoSentences(text).length;
6283
+ function arrayableToArray(input) {
6284
+ if (input === undefined) {
6285
+ return [];
6286
+ }
6287
+ if (input instanceof Array) {
6288
+ return input;
6289
+ }
6290
+ return [input];
6203
6291
  }
6204
- /**
6205
- * TODO: [๐Ÿฅด] Implement counting in formats - like JSON, CSV, XML,...
6206
- */
6207
6292
 
6208
6293
  /**
6209
- * Counts number of words in the text
6294
+ * Replaces parameters in template with values from parameters object
6210
6295
  *
6211
- * @public exported from `@promptbook/utils`
6212
- */
6213
- function countWords(text) {
6214
- text = text.replace(/[\p{Extended_Pictographic}]/gu, 'a');
6215
- text = removeDiacritics(text);
6216
- // Add spaces before uppercase letters preceded by lowercase letters (for camelCase)
6217
- text = text.replace(/([a-z])([A-Z])/g, '$1 $2');
6218
- return text.split(/[^a-zะฐ-ั0-9]+/i).filter((word) => word.length > 0).length;
6219
- }
6220
- /**
6221
- * TODO: [๐Ÿฅด] Implement counting in formats - like JSON, CSV, XML,...
6222
- */
6223
-
6224
- /**
6225
- * Index of all counter functions
6296
+ * Note: This function is not places strings into string,
6297
+ * It's more complex and can handle this operation specifically for LLM models
6226
6298
  *
6299
+ * @param template the template with parameters in {curly} braces
6300
+ * @param parameters the object with parameters
6301
+ * @returns the template with replaced parameters
6302
+ * @throws {PipelineExecutionError} if parameter is not defined, not closed, or not opened
6227
6303
  * @public exported from `@promptbook/utils`
6228
6304
  */
6229
- const CountUtils = {
6230
- CHARACTERS: countCharacters,
6231
- WORDS: countWords,
6232
- SENTENCES: countSentences,
6233
- PARAGRAPHS: countParagraphs,
6234
- LINES: countLines,
6235
- PAGES: countPages,
6236
- };
6237
- /**
6238
- * TODO: [๐Ÿง ][๐Ÿค ] This should be probably as part of `TextFormatParser`
6239
- * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name
6240
- */
6241
-
6242
- /**
6243
- * Function checkExpectations will check if the expectations on given value are met
6244
- *
6245
- * Note: There are two similar functions:
6246
- * - `checkExpectations` which throws an error if the expectations are not met
6247
- * - `isPassingExpectations` which returns a boolean
6248
- *
6249
- * @throws {ExpectError} if the expectations are not met
6250
- * @returns {void} Nothing
6251
- * @private internal function of `createPipelineExecutor`
6252
- */
6253
- function checkExpectations(expectations, value) {
6254
- for (const [unit, { max, min }] of Object.entries(expectations)) {
6255
- const amount = CountUtils[unit.toUpperCase()](value);
6256
- if (min && amount < min) {
6257
- throw new ExpectError(`Expected at least ${min} ${unit} but got ${amount}`);
6258
- } /* not else */
6259
- if (max && amount > max) {
6260
- throw new ExpectError(`Expected at most ${max} ${unit} but got ${amount}`);
6305
+ function templateParameters(template, parameters) {
6306
+ for (const [parameterName, parameterValue] of Object.entries(parameters)) {
6307
+ if (parameterValue === RESERVED_PARAMETER_MISSING_VALUE) {
6308
+ throw new UnexpectedError(`Parameter \`{${parameterName}}\` has missing value`);
6309
+ }
6310
+ else if (parameterValue === RESERVED_PARAMETER_RESTRICTED) {
6311
+ // TODO: [๐Ÿต]
6312
+ throw new UnexpectedError(`Parameter \`{${parameterName}}\` is restricted to use`);
6313
+ }
6314
+ }
6315
+ let replacedTemplates = template;
6316
+ let match;
6317
+ let loopLimit = LOOP_LIMIT;
6318
+ while ((match = /^(?<precol>.*){(?<parameterName>\w+)}(.*)/m /* <- Not global */
6319
+ .exec(replacedTemplates))) {
6320
+ if (loopLimit-- < 0) {
6321
+ throw new LimitReachedError('Loop limit reached during parameters replacement in `templateParameters`');
6322
+ }
6323
+ const precol = match.groups.precol;
6324
+ const parameterName = match.groups.parameterName;
6325
+ if (parameterName === '') {
6326
+ // Note: Skip empty placeholders. It's used to avoid confusion with JSON-like strings
6327
+ continue;
6328
+ }
6329
+ if (parameterName.indexOf('{') !== -1 || parameterName.indexOf('}') !== -1) {
6330
+ throw new PipelineExecutionError('Parameter is already opened or not closed');
6331
+ }
6332
+ if (parameters[parameterName] === undefined) {
6333
+ throw new PipelineExecutionError(`Parameter \`{${parameterName}}\` is not defined`);
6334
+ }
6335
+ let parameterValue = parameters[parameterName];
6336
+ if (parameterValue === undefined) {
6337
+ throw new PipelineExecutionError(`Parameter \`{${parameterName}}\` is not defined`);
6338
+ }
6339
+ parameterValue = valueToString(parameterValue);
6340
+ // Escape curly braces in parameter values to prevent prompt-injection
6341
+ parameterValue = parameterValue.replace(/[{}]/g, '\\$&');
6342
+ if (parameterValue.includes('\n') && /^\s*\W{0,3}\s*$/.test(precol)) {
6343
+ parameterValue = parameterValue
6344
+ .split('\n')
6345
+ .map((line, index) => (index === 0 ? line : `${precol}${line}`))
6346
+ .join('\n');
6261
6347
  }
6348
+ replacedTemplates =
6349
+ replacedTemplates.substring(0, match.index + precol.length) +
6350
+ parameterValue +
6351
+ replacedTemplates.substring(match.index + precol.length + parameterName.length + 2);
6352
+ }
6353
+ // [๐Ÿ’ซ] Check if there are parameters that are not closed properly
6354
+ if (/{\w+$/.test(replacedTemplates)) {
6355
+ throw new PipelineExecutionError('Parameter is not closed');
6356
+ }
6357
+ // [๐Ÿ’ซ] Check if there are parameters that are not opened properly
6358
+ if (/^\w+}/.test(replacedTemplates)) {
6359
+ throw new PipelineExecutionError('Parameter is not opened');
6262
6360
  }
6361
+ return replacedTemplates;
6263
6362
  }
6264
- /**
6265
- * TODO: [๐Ÿ’] Unite object for expecting amount and format
6266
- * TODO: [๐Ÿง ][๐Ÿค ] This should be part of `TextFormatParser`
6267
- * Note: [๐Ÿ’] and [๐Ÿค ] are interconnected together
6268
- */
6269
6363
 
6270
6364
  /**
6271
6365
  * Executes a pipeline task with multiple attempts, including joker and retry logic. Handles different task types
@@ -6289,13 +6383,13 @@ async function executeAttempts(options) {
6289
6383
  // TODO: [๐Ÿš] Make arrayable LLMs -> single LLM DRY
6290
6384
  const _llms = arrayableToArray(tools.llm);
6291
6385
  const llmTools = _llms.length === 1 ? _llms[0] : joinLlmExecutionTools(..._llms);
6292
- attempts: for (let attempt = -jokerParameterNames.length; attempt < maxAttempts; attempt++) {
6293
- const isJokerAttempt = attempt < 0;
6294
- const jokerParameterName = jokerParameterNames[jokerParameterNames.length + attempt];
6386
+ attempts: for (let attemptIndex = -jokerParameterNames.length; attemptIndex < maxAttempts; attemptIndex++) {
6387
+ const isJokerAttempt = attemptIndex < 0;
6388
+ const jokerParameterName = jokerParameterNames[jokerParameterNames.length + attemptIndex];
6295
6389
  // TODO: [๐Ÿง ][๐Ÿญ] JOKERS, EXPECTATIONS, POSTPROCESSING and FOREACH
6296
6390
  if (isJokerAttempt && !jokerParameterName) {
6297
6391
  throw new UnexpectedError(spaceTrim$1((block) => `
6298
- Joker not found in attempt ${attempt}
6392
+ Joker not found in attempt ${attemptIndex}
6299
6393
 
6300
6394
  ${block(pipelineIdentification)}
6301
6395
  `));
@@ -6493,35 +6587,18 @@ async function executeAttempts(options) {
6493
6587
  }
6494
6588
  }
6495
6589
  // TODO: [๐Ÿ’] Unite object for expecting amount and format
6496
- if (task.format) {
6497
- if (task.format === 'JSON') {
6498
- if (!isValidJsonString($ongoingTaskResult.$resultString || '')) {
6499
- // TODO: [๐Ÿข] Do more universally via `FormatParser`
6500
- try {
6501
- $ongoingTaskResult.$resultString = extractJsonBlock($ongoingTaskResult.$resultString || '');
6502
- }
6503
- catch (error) {
6504
- keepUnused(error);
6505
- throw new ExpectError(spaceTrim$1((block) => `
6506
- Expected valid JSON string
6507
-
6508
- ${block(
6509
- /*<- Note: No need for `pipelineIdentification`, it will be catched and added later */ '')}
6510
- `));
6511
- }
6512
- }
6513
- }
6514
- else {
6515
- throw new UnexpectedError(spaceTrim$1((block) => `
6516
- Unknown format "${task.format}"
6517
-
6518
- ${block(pipelineIdentification)}
6519
- `));
6590
+ // Use the common validation function for both format and expectations
6591
+ if (task.format || task.expectations) {
6592
+ const validationResult = validatePromptResult({
6593
+ resultString: $ongoingTaskResult.$resultString || '',
6594
+ expectations: task.expectations,
6595
+ format: task.format,
6596
+ });
6597
+ if (!validationResult.isValid) {
6598
+ throw validationResult.error;
6520
6599
  }
6521
- }
6522
- // TODO: [๐Ÿ’] Unite object for expecting amount and format
6523
- if (task.expectations) {
6524
- checkExpectations(task.expectations, $ongoingTaskResult.$resultString || '');
6600
+ // Update the result string in case format processing modified it (e.g., JSON extraction)
6601
+ $ongoingTaskResult.$resultString = validationResult.processedResultString;
6525
6602
  }
6526
6603
  break attempts;
6527
6604
  }
@@ -6535,6 +6612,7 @@ async function executeAttempts(options) {
6535
6612
  $ongoingTaskResult.$failedResults = [];
6536
6613
  }
6537
6614
  $ongoingTaskResult.$failedResults.push({
6615
+ attemptIndex,
6538
6616
  result: $ongoingTaskResult.$resultString,
6539
6617
  error: error,
6540
6618
  });
@@ -6559,19 +6637,13 @@ async function executeAttempts(options) {
6559
6637
  });
6560
6638
  }
6561
6639
  }
6562
- if ($ongoingTaskResult.$expectError !== null && attempt === maxAttempts - 1) {
6563
- // Store the current failure before throwing
6564
- $ongoingTaskResult.$failedResults = $ongoingTaskResult.$failedResults || [];
6565
- $ongoingTaskResult.$failedResults.push({
6566
- result: $ongoingTaskResult.$resultString,
6567
- error: $ongoingTaskResult.$expectError,
6568
- });
6569
- // Create a summary of all failures
6640
+ if ($ongoingTaskResult.$expectError !== null && attemptIndex === maxAttempts - 1) {
6641
+ // Note: Create a summary of all failures
6570
6642
  const failuresSummary = $ongoingTaskResult.$failedResults
6571
- .map((failure, index) => spaceTrim$1((block) => {
6643
+ .map((failure) => spaceTrim$1((block) => {
6572
6644
  var _a, _b;
6573
6645
  return `
6574
- Attempt ${index + 1}:
6646
+ Attempt ${failure.attemptIndex + 1}:
6575
6647
  Error ${((_a = failure.error) === null || _a === void 0 ? void 0 : _a.name) || ''}:
6576
6648
  ${block((_b = failure.error) === null || _b === void 0 ? void 0 : _b.message.split('\n').map((line) => `> ${line}`).join('\n'))}
6577
6649
 
@@ -17323,7 +17395,7 @@ resultContent, rawResponse) {
17323
17395
  */
17324
17396
 
17325
17397
  /**
17326
- * Execution Tools for calling OpenAI API or other OpeenAI compatible provider
17398
+ * Execution Tools for calling OpenAI API or other OpenAI compatible provider
17327
17399
  *
17328
17400
  * @public exported from `@promptbook/openai`
17329
17401
  */
@@ -17893,6 +17965,7 @@ class OllamaExecutionTools extends OpenAiCompatibleExecutionTools {
17893
17965
  baseURL: DEFAULT_OLLAMA_BASE_URL,
17894
17966
  ...ollamaOptions,
17895
17967
  apiKey: 'ollama',
17968
+ isProxied: false, // <- Note: Ollama is always local
17896
17969
  };
17897
17970
  super(openAiCompatibleOptions);
17898
17971
  }
@@ -18079,7 +18152,7 @@ const _OpenAiCompatibleMetadataRegistration = $llmToolsMetadataRegister.register
18079
18152
  title: 'Open AI Compatible',
18080
18153
  packageName: '@promptbook/openai',
18081
18154
  className: 'OpenAiCompatibleExecutionTools',
18082
- envVariables: ['OPENAI_API_KEY'],
18155
+ envVariables: ['OPENAI_API_KEY', 'OPENAI_BASE_URL'],
18083
18156
  trustLevel: 'CLOSED',
18084
18157
  order: MODEL_ORDERS.TOP_TIER,
18085
18158
  getBoilerplateConfiguration() {
@@ -18089,11 +18162,35 @@ const _OpenAiCompatibleMetadataRegistration = $llmToolsMetadataRegister.register
18089
18162
  className: 'OpenAiCompatibleExecutionTools',
18090
18163
  options: {
18091
18164
  apiKey: 'sk-',
18165
+ baseURL: 'https://api.openai.com/v1',
18166
+ isProxied: false,
18167
+ remoteServerUrl: DEFAULT_REMOTE_SERVER_URL,
18092
18168
  maxRequestsPerMinute: DEFAULT_MAX_REQUESTS_PER_MINUTE,
18093
18169
  },
18094
18170
  };
18095
18171
  },
18096
18172
  createConfigurationFromEnv(env) {
18173
+ // Note: OpenAiCompatibleExecutionTools is an abstract class and cannot be instantiated directly
18174
+ // However, we can provide configuration for users who want to manually instantiate it
18175
+ if (typeof env.OPENAI_API_KEY === 'string') {
18176
+ const options = {
18177
+ apiKey: env.OPENAI_API_KEY,
18178
+ isProxied: false,
18179
+ remoteServerUrl: DEFAULT_REMOTE_SERVER_URL,
18180
+ maxRequestsPerMinute: DEFAULT_MAX_REQUESTS_PER_MINUTE,
18181
+ defaultModelName: 'gpt-4-turbo',
18182
+ };
18183
+ // Add baseURL if provided in environment
18184
+ if (typeof env.OPENAI_BASE_URL === 'string') {
18185
+ options.baseURL = env.OPENAI_BASE_URL;
18186
+ }
18187
+ return {
18188
+ title: 'Open AI Compatible (from env)',
18189
+ packageName: '@promptbook/openai',
18190
+ className: 'OpenAiCompatibleExecutionTools',
18191
+ options,
18192
+ };
18193
+ }
18097
18194
  return null;
18098
18195
  },
18099
18196
  });
@@ -18146,7 +18243,7 @@ class OpenAiExecutionTools extends OpenAiCompatibleExecutionTools {
18146
18243
  * Default model for chat variant.
18147
18244
  */
18148
18245
  getDefaultChatModel() {
18149
- return this.getDefaultModel('gpt-4o');
18246
+ return this.getDefaultModel('gpt-4-turbo');
18150
18247
  }
18151
18248
  /**
18152
18249
  * Default model for completion variant.
@@ -18176,6 +18273,9 @@ class OpenAiAssistantExecutionTools extends OpenAiExecutionTools {
18176
18273
  * @param options which are relevant are directly passed to the OpenAI client
18177
18274
  */
18178
18275
  constructor(options) {
18276
+ if (options.isProxied) {
18277
+ throw new NotYetImplementedError(`Proxy mode is not yet implemented for OpenAI assistants`);
18278
+ }
18179
18279
  super(options);
18180
18280
  this.assistantId = options.assistantId;
18181
18281
  // TODO: [๐Ÿ‘ฑ] Make limiter same as in `OpenAiExecutionTools`
@@ -18357,14 +18457,97 @@ const createOpenAiAssistantExecutionTools = Object.assign((options) => {
18357
18457
  * @public exported from `@promptbook/openai`
18358
18458
  */
18359
18459
  const createOpenAiCompatibleExecutionTools = Object.assign((options) => {
18460
+ if (options.isProxied) {
18461
+ return new RemoteLlmExecutionTools({
18462
+ ...options,
18463
+ identification: {
18464
+ isAnonymous: true,
18465
+ llmToolsConfiguration: [
18466
+ {
18467
+ title: 'OpenAI Compatible (proxied)',
18468
+ packageName: '@promptbook/openai',
18469
+ className: 'OpenAiCompatibleExecutionTools',
18470
+ options: {
18471
+ ...options,
18472
+ isProxied: false,
18473
+ },
18474
+ },
18475
+ ],
18476
+ },
18477
+ });
18478
+ }
18360
18479
  if (($isRunningInBrowser() || $isRunningInWebWorker()) && !options.dangerouslyAllowBrowser) {
18361
18480
  options = { ...options, dangerouslyAllowBrowser: true };
18362
18481
  }
18363
- return new OpenAiExecutionTools(options);
18482
+ return new HardcodedOpenAiCompatibleExecutionTools(options.defaultModelName, options);
18364
18483
  }, {
18365
18484
  packageName: '@promptbook/openai',
18366
18485
  className: 'OpenAiCompatibleExecutionTools',
18367
18486
  });
18487
+ /**
18488
+ * Execution Tools for calling ONE SPECIFIC PRECONFIGURED OpenAI compatible provider
18489
+ *
18490
+ * @private for `createOpenAiCompatibleExecutionTools`
18491
+ */
18492
+ class HardcodedOpenAiCompatibleExecutionTools extends OpenAiCompatibleExecutionTools {
18493
+ /**
18494
+ * Creates OpenAI compatible Execution Tools.
18495
+ *
18496
+ * @param options which are relevant are directly passed to the OpenAI compatible client
18497
+ */
18498
+ constructor(defaultModelName, options) {
18499
+ super(options);
18500
+ this.defaultModelName = defaultModelName;
18501
+ this.options = options;
18502
+ }
18503
+ get title() {
18504
+ return `${this.defaultModelName} on ${this.options.baseURL}`;
18505
+ }
18506
+ get description() {
18507
+ return `OpenAI compatible connected to "${this.options.baseURL}" model "${this.defaultModelName}"`;
18508
+ }
18509
+ /**
18510
+ * List all available models (non dynamically)
18511
+ *
18512
+ * Note: Purpose of this is to provide more information about models than standard listing from API
18513
+ */
18514
+ get HARDCODED_MODELS() {
18515
+ return [
18516
+ {
18517
+ modelName: this.defaultModelName,
18518
+ modelVariant: 'CHAT',
18519
+ modelDescription: '', // <- TODO: What is the best value here, maybe `this.description`?
18520
+ },
18521
+ ];
18522
+ }
18523
+ /**
18524
+ * Computes the usage
18525
+ */
18526
+ computeUsage(...args) {
18527
+ return {
18528
+ ...computeOpenAiUsage(...args),
18529
+ price: UNCERTAIN_ZERO_VALUE, // <- TODO: Maybe in future pass this counting mechanism, but for now, we dont know
18530
+ };
18531
+ }
18532
+ /**
18533
+ * Default model for chat variant.
18534
+ */
18535
+ getDefaultChatModel() {
18536
+ return this.getDefaultModel(this.defaultModelName);
18537
+ }
18538
+ /**
18539
+ * Default model for completion variant.
18540
+ */
18541
+ getDefaultCompletionModel() {
18542
+ throw new PipelineExecutionError(`${this.title} does not support COMPLETION model variant`);
18543
+ }
18544
+ /**
18545
+ * Default model for completion variant.
18546
+ */
18547
+ getDefaultEmbeddingModel() {
18548
+ throw new PipelineExecutionError(`${this.title} does not support EMBEDDING model variant`);
18549
+ }
18550
+ }
18368
18551
  /**
18369
18552
  * TODO: [๐Ÿฆบ] Is there some way how to put `packageName` and `className` on top and function definition on bottom?
18370
18553
  * TODO: [๐ŸŽถ] Naming "constructor" vs "creator" vs "factory"
@@ -18381,6 +18564,9 @@ const createOpenAiExecutionTools = Object.assign((options) => {
18381
18564
  if (($isRunningInBrowser() || $isRunningInWebWorker()) && !options.dangerouslyAllowBrowser) {
18382
18565
  options = { ...options, dangerouslyAllowBrowser: true };
18383
18566
  }
18567
+ if (options.isProxied) {
18568
+ throw new NotYetImplementedError(`Proxy mode is not yet implemented in createOpenAiExecutionTools`);
18569
+ }
18384
18570
  return new OpenAiExecutionTools(options);
18385
18571
  }, {
18386
18572
  packageName: '@promptbook/openai',