@promptbook/node 0.112.0-70 → 0.112.0-72

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 (45) hide show
  1. package/esm/index.es.js +1403 -1272
  2. package/esm/index.es.js.map +1 -1
  3. package/esm/src/book-3.0/Book.d.ts +6 -0
  4. package/esm/src/book-components/Chat/utils/getToolCallChipletInfo.test.d.ts +1 -0
  5. package/esm/src/cli/cli-commands/agent/agentRunCliOptions.d.ts +12 -2
  6. package/esm/src/cli/cli-commands/agent/initializeAgentRunnerCommand.d.ts +1 -0
  7. package/esm/src/cli/cli-commands/run/prepareRunCommandResources.d.ts +20 -0
  8. package/esm/src/cli/cli-commands/run/resolveRunInputParameters.d.ts +12 -0
  9. package/esm/src/cli/cli-commands/run/runCommandAction.d.ts +21 -0
  10. package/esm/src/cli/cli-commands/run/runPipelineExecution.d.ts +14 -0
  11. package/esm/src/cli/cli-commands/run.d.ts +1 -1
  12. package/esm/src/conversion/parsePipeline/applyPipelineHead.d.ts +8 -0
  13. package/esm/src/conversion/parsePipeline/createInitialPipelineJson.d.ts +8 -0
  14. package/esm/src/conversion/parsePipeline/createUniqueSectionNameResolver.d.ts +14 -0
  15. package/esm/src/conversion/parsePipeline/defineParameter.d.ts +8 -0
  16. package/esm/src/conversion/parsePipeline/extractPipelineDescription.d.ts +6 -0
  17. package/esm/src/conversion/parsePipeline/finalizeParsedPipeline.d.ts +8 -0
  18. package/esm/src/conversion/parsePipeline/getPipelineIdentification.d.ts +7 -0
  19. package/esm/src/conversion/parsePipeline/parsePreparedPipelineSections.d.ts +18 -0
  20. package/esm/src/conversion/parsePipeline/preparePipelineString.d.ts +8 -0
  21. package/esm/src/conversion/parsePipeline/processPipelineSection.d.ts +9 -0
  22. package/esm/src/version.d.ts +1 -1
  23. package/package.json +2 -2
  24. package/umd/index.umd.js +1403 -1272
  25. package/umd/index.umd.js.map +1 -1
  26. package/umd/src/book-3.0/Book.d.ts +6 -0
  27. package/umd/src/book-components/Chat/utils/getToolCallChipletInfo.test.d.ts +1 -0
  28. package/umd/src/cli/cli-commands/agent/agentRunCliOptions.d.ts +12 -2
  29. package/umd/src/cli/cli-commands/agent/initializeAgentRunnerCommand.d.ts +1 -0
  30. package/umd/src/cli/cli-commands/run/prepareRunCommandResources.d.ts +20 -0
  31. package/umd/src/cli/cli-commands/run/resolveRunInputParameters.d.ts +12 -0
  32. package/umd/src/cli/cli-commands/run/runCommandAction.d.ts +21 -0
  33. package/umd/src/cli/cli-commands/run/runPipelineExecution.d.ts +14 -0
  34. package/umd/src/cli/cli-commands/run.d.ts +1 -1
  35. package/umd/src/conversion/parsePipeline/applyPipelineHead.d.ts +8 -0
  36. package/umd/src/conversion/parsePipeline/createInitialPipelineJson.d.ts +8 -0
  37. package/umd/src/conversion/parsePipeline/createUniqueSectionNameResolver.d.ts +14 -0
  38. package/umd/src/conversion/parsePipeline/defineParameter.d.ts +8 -0
  39. package/umd/src/conversion/parsePipeline/extractPipelineDescription.d.ts +6 -0
  40. package/umd/src/conversion/parsePipeline/finalizeParsedPipeline.d.ts +8 -0
  41. package/umd/src/conversion/parsePipeline/getPipelineIdentification.d.ts +7 -0
  42. package/umd/src/conversion/parsePipeline/parsePreparedPipelineSections.d.ts +18 -0
  43. package/umd/src/conversion/parsePipeline/preparePipelineString.d.ts +8 -0
  44. package/umd/src/conversion/parsePipeline/processPipelineSection.d.ts +9 -0
  45. package/umd/src/version.d.ts +1 -1
package/umd/index.umd.js CHANGED
@@ -48,7 +48,7 @@
48
48
  * @generated
49
49
  * @see https://github.com/webgptorg/promptbook
50
50
  */
51
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-70';
51
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-72';
52
52
  /**
53
53
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
54
54
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -7621,118 +7621,75 @@
7621
7621
  */
7622
7622
 
7623
7623
  /**
7624
- * All available task types
7625
- *
7626
- * There is is distinction between task types and section types
7627
- * - Every section in markdown has its SectionType
7628
- * - Some sections are tasks but other can be non-task sections
7629
- *
7630
- * @public exported from `@promptbook/core`
7631
- */
7632
- const TaskTypes = [
7633
- 'PROMPT',
7634
- 'SIMPLE',
7635
- 'SCRIPT',
7636
- 'DIALOG',
7637
- // <- [🅱]
7638
- ];
7639
-
7640
- /**
7641
- * All available sections which are not tasks
7642
- *
7643
- * @public exported from `@promptbook/core`
7644
- */
7645
- const NonTaskSectionTypes = ['EXAMPLE', 'KNOWLEDGE', 'INSTRUMENT', 'ACTION'];
7646
- /**
7647
- * All available section types
7648
- *
7649
- * There is is distinction between task types and section types
7650
- * - Every section in markdown has its SectionType
7651
- * - Some sections are tasks but other can be non-task sections
7624
+ * Parses the boilerplate command
7652
7625
  *
7653
- * @public exported from `@promptbook/core`
7654
- */
7655
- const SectionTypes = [
7656
- ...TaskTypes.map((TaskType) => `${TaskType}_TASK`),
7657
- ...NonTaskSectionTypes,
7658
- ];
7659
-
7660
- /**
7661
- * Parses the knowledge command
7626
+ * Note: @@ This command is used as boilerplate for new commands - it should NOT be used in any `.book` file
7662
7627
  *
7663
7628
  * @see `documentationUrl` for more details
7664
7629
  *
7665
- * @public exported from `@promptbook/editable`
7630
+ * @private within the commands folder
7666
7631
  */
7667
- const knowledgeCommandParser = {
7632
+ const boilerplateCommandParser = {
7668
7633
  /**
7669
7634
  * Name of the command
7670
7635
  */
7671
- name: 'KNOWLEDGE',
7636
+ name: 'BOILERPLATE',
7637
+ /**
7638
+ * Aliases for the BOILERPLATE command
7639
+ */
7640
+ aliasNames: ['BP'],
7672
7641
  /**
7673
7642
  * BOILERPLATE command can be used in:
7674
7643
  */
7675
7644
  isUsedInPipelineHead: true,
7676
- isUsedInPipelineTask: false,
7645
+ isUsedInPipelineTask: true,
7677
7646
  /**
7678
- * Description of the KNOWLEDGE command
7647
+ * Description of the BOILERPLATE command
7679
7648
  */
7680
- description: `Tells promptbook which external knowledge to use`,
7649
+ description: `@@`,
7681
7650
  /**
7682
7651
  * Link to documentation
7683
7652
  */
7684
- documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/41',
7653
+ documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/@@',
7685
7654
  /**
7686
- * Example usages of the KNOWLEDGE command
7655
+ * Example usages of the BOILERPLATE command
7687
7656
  */
7688
- examples: [
7689
- 'KNOWLEDGE https://www.pavolhejny.com/',
7690
- 'KNOWLEDGE ./hejny-cv.txt',
7691
- 'KNOWLEDGE ./hejny-cv.md',
7692
- 'KNOWLEDGE ./hejny-cv.pdf',
7693
- 'KNOWLEDGE ./hejny-cv.docx',
7694
- // <- TODO: [😿] Allow ONLY files scoped in the (sub)directory NOT ../ and test it
7695
- ],
7657
+ examples: ['BOILERPLATE foo', 'BOILERPLATE bar', 'BP foo', 'BP bar'],
7696
7658
  /**
7697
- * Parses the KNOWLEDGE command
7659
+ * Parses the BOILERPLATE command
7698
7660
  */
7699
7661
  parse(input) {
7700
7662
  const { args } = input;
7701
- const knowledgeSourceContent = _spaceTrim.spaceTrim(args[0] || '');
7702
- if (knowledgeSourceContent === '') {
7703
- throw new ParseError(`Source is not defined`);
7704
- }
7705
- // TODO: [main] !!4 Following checks should be applied every link in the `sourceContent`
7706
- if (knowledgeSourceContent.startsWith('http://')) {
7707
- throw new ParseError(`Source is not secure`);
7708
- }
7709
- if (!(isValidFilePath(knowledgeSourceContent) || isValidUrl(knowledgeSourceContent))) {
7710
- throw new ParseError(`Source not valid`);
7663
+ if (args.length !== 1) {
7664
+ throw new ParseError(`BOILERPLATE command requires exactly one argument`);
7711
7665
  }
7712
- if (knowledgeSourceContent.startsWith('../') ||
7713
- knowledgeSourceContent.startsWith('/') ||
7714
- /^[A-Z]:[\\/]+/i.test(knowledgeSourceContent)) {
7715
- throw new ParseError(`Source cannot be outside of the .book.md folder`);
7666
+ const value = args[0].toLowerCase();
7667
+ if (value.includes('brr')) {
7668
+ throw new ParseError(`BOILERPLATE value can not contain brr`);
7716
7669
  }
7717
7670
  return {
7718
- type: 'KNOWLEDGE',
7719
- knowledgeSourceContent,
7671
+ type: 'BOILERPLATE',
7672
+ value,
7720
7673
  };
7721
7674
  },
7722
7675
  /**
7723
- * Apply the KNOWLEDGE command to the `pipelineJson`
7676
+ * Apply the BOILERPLATE command to the `pipelineJson`
7724
7677
  *
7725
7678
  * Note: `$` is used to indicate that this function mutates given `pipelineJson`
7726
7679
  */
7727
7680
  $applyToPipelineJson(command, $pipelineJson) {
7728
- const { knowledgeSourceContent } = command;
7729
- $pipelineJson.knowledgeSources.push({
7730
- name: knowledgeSourceContentToName(knowledgeSourceContent),
7731
- knowledgeSourceContent,
7732
- });
7681
+ throw new ParseError(`BOILERPLATE command is only for testing purposes and should not be used in the .book.md file`);
7733
7682
  },
7734
7683
  /**
7735
- * Converts the KNOWLEDGE command back to string
7684
+ * Apply the BOILERPLATE command to the `pipelineJson`
7685
+ *
7686
+ * Note: `$` is used to indicate that this function mutates given `taskJson`
7687
+ */
7688
+ $applyToTaskJson(command, $taskJson, $pipelineJson) {
7689
+ throw new ParseError(`BOILERPLATE command is only for testing purposes and should not be used in the .book.md file`);
7690
+ },
7691
+ /**
7692
+ * Converts the BOILERPLATE command back to string
7736
7693
  *
7737
7694
  * Note: This is used in `pipelineJsonToString` utility
7738
7695
  */
@@ -7740,354 +7697,77 @@
7740
7697
  return `---`; // <- TODO: [🛋] Implement
7741
7698
  },
7742
7699
  /**
7743
- * Reads the KNOWLEDGE command from the `PipelineJson`
7700
+ * Reads the BOILERPLATE command from the `PipelineJson`
7744
7701
  *
7745
7702
  * Note: This is used in `pipelineJsonToString` utility
7746
7703
  */
7747
7704
  takeFromPipelineJson(pipelineJson) {
7748
- throw new NotYetImplementedError(`[🛋] Not implemented yet`); // <- TODO: [🛋] Implement
7705
+ throw new ParseError(`BOILERPLATE command is only for testing purposes and should not be used in the .book.md file`);
7706
+ },
7707
+ /**
7708
+ * Reads the BOILERPLATE command from the `TaskJson`
7709
+ *
7710
+ * Note: This is used in `pipelineJsonToString` utility
7711
+ */
7712
+ takeFromTaskJson($taskJson) {
7713
+ throw new ParseError(`BOILERPLATE command is only for testing purposes and should not be used in the .book.md file`);
7749
7714
  },
7750
7715
  };
7751
- // Note: [⛱] There are two types of KNOWLEDGE commands *...(read more in [⛱])*
7752
7716
 
7753
7717
  /**
7754
- * Parses the section command
7718
+ * Parses the BOOK_VERSION command
7755
7719
  *
7756
7720
  * @see `documentationUrl` for more details
7757
7721
  *
7758
7722
  * @public exported from `@promptbook/editable`
7759
7723
  */
7760
- const sectionCommandParser = {
7724
+ const bookVersionCommandParser = {
7761
7725
  /**
7762
7726
  * Name of the command
7763
7727
  */
7764
- name: 'SECTION',
7765
- /**
7766
- * Aliases for the SECTION command
7767
- */
7768
- aliasNames: [
7769
- 'PROMPT',
7770
- 'SIMPLE',
7771
- 'SCRIPT',
7772
- 'DIALOG',
7773
- 'SAMPLE',
7774
- 'EXAMPLE',
7775
- 'KNOWLEDGE',
7776
- 'INSTRUMENT',
7777
- 'ACTION', // <- Note: [⛱]
7778
- ],
7779
- /**
7780
- * Aliases for the SECTION command
7781
- */
7782
- deprecatedNames: ['TEMPLATE', 'BLOCK', 'EXECUTE'],
7728
+ name: 'BOOK_VERSION',
7729
+ aliasNames: ['PTBK_VERSION', 'PROMPTBOOK_VERSION', 'BOOK'],
7783
7730
  /**
7784
7731
  * BOILERPLATE command can be used in:
7785
7732
  */
7786
- isUsedInPipelineHead: false,
7787
- isUsedInPipelineTask: true,
7733
+ isUsedInPipelineHead: true,
7734
+ isUsedInPipelineTask: false,
7788
7735
  /**
7789
- * Description of the SECTION command
7736
+ * Description of the BOOK_VERSION command
7790
7737
  */
7791
- description: `Defines the purpose of the markdown section - if its a task and which type or something else`,
7738
+ description: `Which version of the Book language is the .book.md using`,
7792
7739
  /**
7793
7740
  * Link to documentation
7794
7741
  */
7795
- documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/64',
7742
+ documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/69',
7796
7743
  /**
7797
- * Example usages of the SECTION command
7744
+ * Example usages of the BOOK_VERSION command
7798
7745
  */
7799
- examples: [
7800
- // Short form:
7801
- 'PROMPT',
7802
- 'SIMPLE',
7803
- 'SCRIPT',
7804
- 'DIALOG',
7805
- // <- [🅱]
7806
- 'EXAMPLE',
7807
- 'KNOWLEDGE',
7808
- 'INSTRUMENT',
7809
- 'ACTION',
7810
- // -----------------
7811
- // Recommended (reversed) form:
7812
- 'PROMPT SECTION',
7813
- 'SIMPLE SECTION',
7814
- 'SCRIPT SECTION',
7815
- 'DIALOG SECTION',
7816
- // <- [🅱]
7817
- 'EXAMPLE SECTION',
7818
- 'KNOWLEDGE SECTION',
7819
- 'INSTRUMENT SECTION',
7820
- 'ACTION SECTION',
7821
- // -----------------
7822
- // Standard form:
7823
- 'SECTION PROMPT',
7824
- 'SECTION SIMPLE',
7825
- 'SECTION SCRIPT',
7826
- 'SECTION DIALOG',
7827
- // <- [🅱]
7828
- 'SECTION EXAMPLE',
7829
- 'SECTION KNOWLEDGE',
7830
- 'SECTION INSTRUMENT',
7831
- 'SECTION ACTION',
7832
- ],
7833
- // TODO: [♓️] order: -10 /* <- Note: Putting before other commands */
7746
+ examples: [`BOOK VERSION ${BOOK_LANGUAGE_VERSION}`, `BOOK ${BOOK_LANGUAGE_VERSION}`],
7834
7747
  /**
7835
- * Parses the SECTION command
7748
+ * Parses the BOOK_VERSION command
7836
7749
  */
7837
7750
  parse(input) {
7838
- let { normalized } = input;
7839
- normalized = normalized.split('SAMPLE').join('EXAMPLE');
7840
- normalized = normalized.split('EXECUTE_').join('');
7841
- normalized = normalized.split('DIALOGUE').join('DIALOG');
7842
- const taskTypes = SectionTypes.filter((sectionType) => normalized.includes(sectionType.split('_TASK').join('')));
7843
- if (taskTypes.length !== 1) {
7844
- throw new ParseError(_spaceTrim.spaceTrim((block) => `
7845
- Unknown section type "${normalized}"
7846
-
7847
- Supported section types are:
7848
- ${block(SectionTypes.join(', '))}
7849
- `));
7751
+ const { args } = input;
7752
+ const bookVersion = args.pop();
7753
+ if (bookVersion === undefined) {
7754
+ throw new ParseError(`Version is required`);
7755
+ }
7756
+ if (!isValidPromptbookVersion(bookVersion)) {
7757
+ throw new ParseError(`Invalid Promptbook version "${bookVersion}"`);
7758
+ }
7759
+ if (args.length > 0 && !(((args.length === 1 && args[0]) || '').toUpperCase() === 'VERSION')) {
7760
+ throw new ParseError(`Can not have more than one Promptbook version`);
7850
7761
  }
7851
- const taskType = taskTypes[0];
7852
7762
  return {
7853
- type: 'SECTION',
7854
- taskType,
7763
+ type: 'BOOK_VERSION',
7764
+ bookVersion: bookVersion,
7855
7765
  };
7856
7766
  },
7857
7767
  /**
7858
- * Apply the SECTION command to the `pipelineJson`
7768
+ * Apply the BOOK_VERSION command to the `pipelineJson`
7859
7769
  *
7860
- * Note: `$` is used to indicate that this function mutates given `taskJson`
7861
- */
7862
- $applyToTaskJson(command, $taskJson, $pipelineJson) {
7863
- if ($taskJson.isSectionTypeSet === true) {
7864
- throw new ParseError(_spaceTrim.spaceTrim(`
7865
- Section type is already defined in the section.
7866
- It can be defined only once.
7867
- `));
7868
- }
7869
- $taskJson.isSectionTypeSet = true;
7870
- // TODO: [🍧][💩] Rearrange better - but at bottom and unwrap from function
7871
- const expectResultingParameterName = () => {
7872
- if ($taskJson.resultingParameterName) {
7873
- return;
7874
- }
7875
- throw new ParseError(`Task section and example section must end with return statement -> {parameterName}`);
7876
- };
7877
- if ($taskJson.content === undefined) {
7878
- throw new UnexpectedError(`Content is missing in the taskJson - probably commands are applied in wrong order`);
7879
- }
7880
- if (command.taskType === 'EXAMPLE') {
7881
- expectResultingParameterName();
7882
- const parameter = $pipelineJson.parameters.find((param) => param.name === $taskJson.resultingParameterName);
7883
- if (parameter === undefined) {
7884
- // TODO: !!6 Change to logic error for higher level abstraction of chatbot to work
7885
- throw new ParseError(`Parameter \`{${$taskJson.resultingParameterName}}\` is not defined so can not define example value of it`);
7886
- }
7887
- parameter.exampleValues = parameter.exampleValues || [];
7888
- parameter.exampleValues.push($taskJson.content);
7889
- $taskJson.isTask = false;
7890
- return;
7891
- }
7892
- if (command.taskType === 'KNOWLEDGE') {
7893
- knowledgeCommandParser.$applyToPipelineJson({
7894
- type: 'KNOWLEDGE',
7895
- knowledgeSourceContent: $taskJson.content, // <- TODO: [🐝][main] !!3 Work with KNOWLEDGE which not referring to the source file or website, but its content itself
7896
- }, $pipelineJson);
7897
- $taskJson.isTask = false;
7898
- return;
7899
- }
7900
- if (command.taskType === 'ACTION') {
7901
- console.error(new NotYetImplementedError('Actions are not implemented yet'));
7902
- $taskJson.isTask = false;
7903
- return;
7904
- }
7905
- if (command.taskType === 'INSTRUMENT') {
7906
- console.error(new NotYetImplementedError('Instruments are not implemented yet'));
7907
- $taskJson.isTask = false;
7908
- return;
7909
- }
7910
- expectResultingParameterName();
7911
- $taskJson.taskType = command.taskType;
7912
- $taskJson.isTask = true;
7913
- },
7914
- /**
7915
- * Converts the SECTION command back to string
7916
- *
7917
- * Note: This is used in `pipelineJsonToString` utility
7918
- */
7919
- stringify(command) {
7920
- return `---`; // <- TODO: [🛋] Implement
7921
- },
7922
- /**
7923
- * Reads the SECTION command from the `TaskJson`
7924
- *
7925
- * Note: This is used in `pipelineJsonToString` utility
7926
- */
7927
- takeFromTaskJson($taskJson) {
7928
- throw new NotYetImplementedError(`[🛋] Not implemented yet`); // <- TODO: [🛋] Implement
7929
- },
7930
- };
7931
- /**
7932
- * Note: [⛱] There are two types of KNOWLEDGE, ACTION and INSTRUMENT commands:
7933
- * 1) There are commands `KNOWLEDGE`, `ACTION` and `INSTRUMENT` used in the pipeline head, they just define the knowledge, action or instrument as single line after the command
7934
- * - KNOWLEDGE Look at https://en.wikipedia.org/wiki/Artificial_intelligence
7935
- * 2) `KNOWLEDGE SECTION` which has short form `KNOWLEDGE` is used in the sectiom, does not refer the line itself, but the content of the section block
7936
- * - KNOWLEDGE SECTION
7937
- *
7938
- * ```
7939
- * Look at https://en.wikipedia.org/wiki/Artificial_intelligence
7940
- * ```
7941
- */
7942
-
7943
- /**
7944
- * Parses the boilerplate command
7945
- *
7946
- * Note: @@ This command is used as boilerplate for new commands - it should NOT be used in any `.book` file
7947
- *
7948
- * @see `documentationUrl` for more details
7949
- *
7950
- * @private within the commands folder
7951
- */
7952
- const boilerplateCommandParser = {
7953
- /**
7954
- * Name of the command
7955
- */
7956
- name: 'BOILERPLATE',
7957
- /**
7958
- * Aliases for the BOILERPLATE command
7959
- */
7960
- aliasNames: ['BP'],
7961
- /**
7962
- * BOILERPLATE command can be used in:
7963
- */
7964
- isUsedInPipelineHead: true,
7965
- isUsedInPipelineTask: true,
7966
- /**
7967
- * Description of the BOILERPLATE command
7968
- */
7969
- description: `@@`,
7970
- /**
7971
- * Link to documentation
7972
- */
7973
- documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/@@',
7974
- /**
7975
- * Example usages of the BOILERPLATE command
7976
- */
7977
- examples: ['BOILERPLATE foo', 'BOILERPLATE bar', 'BP foo', 'BP bar'],
7978
- /**
7979
- * Parses the BOILERPLATE command
7980
- */
7981
- parse(input) {
7982
- const { args } = input;
7983
- if (args.length !== 1) {
7984
- throw new ParseError(`BOILERPLATE command requires exactly one argument`);
7985
- }
7986
- const value = args[0].toLowerCase();
7987
- if (value.includes('brr')) {
7988
- throw new ParseError(`BOILERPLATE value can not contain brr`);
7989
- }
7990
- return {
7991
- type: 'BOILERPLATE',
7992
- value,
7993
- };
7994
- },
7995
- /**
7996
- * Apply the BOILERPLATE command to the `pipelineJson`
7997
- *
7998
- * Note: `$` is used to indicate that this function mutates given `pipelineJson`
7999
- */
8000
- $applyToPipelineJson(command, $pipelineJson) {
8001
- throw new ParseError(`BOILERPLATE command is only for testing purposes and should not be used in the .book.md file`);
8002
- },
8003
- /**
8004
- * Apply the BOILERPLATE command to the `pipelineJson`
8005
- *
8006
- * Note: `$` is used to indicate that this function mutates given `taskJson`
8007
- */
8008
- $applyToTaskJson(command, $taskJson, $pipelineJson) {
8009
- throw new ParseError(`BOILERPLATE command is only for testing purposes and should not be used in the .book.md file`);
8010
- },
8011
- /**
8012
- * Converts the BOILERPLATE command back to string
8013
- *
8014
- * Note: This is used in `pipelineJsonToString` utility
8015
- */
8016
- stringify(command) {
8017
- return `---`; // <- TODO: [🛋] Implement
8018
- },
8019
- /**
8020
- * Reads the BOILERPLATE command from the `PipelineJson`
8021
- *
8022
- * Note: This is used in `pipelineJsonToString` utility
8023
- */
8024
- takeFromPipelineJson(pipelineJson) {
8025
- throw new ParseError(`BOILERPLATE command is only for testing purposes and should not be used in the .book.md file`);
8026
- },
8027
- /**
8028
- * Reads the BOILERPLATE command from the `TaskJson`
8029
- *
8030
- * Note: This is used in `pipelineJsonToString` utility
8031
- */
8032
- takeFromTaskJson($taskJson) {
8033
- throw new ParseError(`BOILERPLATE command is only for testing purposes and should not be used in the .book.md file`);
8034
- },
8035
- };
8036
-
8037
- /**
8038
- * Parses the BOOK_VERSION command
8039
- *
8040
- * @see `documentationUrl` for more details
8041
- *
8042
- * @public exported from `@promptbook/editable`
8043
- */
8044
- const bookVersionCommandParser = {
8045
- /**
8046
- * Name of the command
8047
- */
8048
- name: 'BOOK_VERSION',
8049
- aliasNames: ['PTBK_VERSION', 'PROMPTBOOK_VERSION', 'BOOK'],
8050
- /**
8051
- * BOILERPLATE command can be used in:
8052
- */
8053
- isUsedInPipelineHead: true,
8054
- isUsedInPipelineTask: false,
8055
- /**
8056
- * Description of the BOOK_VERSION command
8057
- */
8058
- description: `Which version of the Book language is the .book.md using`,
8059
- /**
8060
- * Link to documentation
8061
- */
8062
- documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/69',
8063
- /**
8064
- * Example usages of the BOOK_VERSION command
8065
- */
8066
- examples: [`BOOK VERSION ${BOOK_LANGUAGE_VERSION}`, `BOOK ${BOOK_LANGUAGE_VERSION}`],
8067
- /**
8068
- * Parses the BOOK_VERSION command
8069
- */
8070
- parse(input) {
8071
- const { args } = input;
8072
- const bookVersion = args.pop();
8073
- if (bookVersion === undefined) {
8074
- throw new ParseError(`Version is required`);
8075
- }
8076
- if (!isValidPromptbookVersion(bookVersion)) {
8077
- throw new ParseError(`Invalid Promptbook version "${bookVersion}"`);
8078
- }
8079
- if (args.length > 0 && !(((args.length === 1 && args[0]) || '').toUpperCase() === 'VERSION')) {
8080
- throw new ParseError(`Can not have more than one Promptbook version`);
8081
- }
8082
- return {
8083
- type: 'BOOK_VERSION',
8084
- bookVersion: bookVersion,
8085
- };
8086
- },
8087
- /**
8088
- * Apply the BOOK_VERSION command to the `pipelineJson`
8089
- *
8090
- * Note: `$` is used to indicate that this function mutates given `pipelineJson`
7770
+ * Note: `$` is used to indicate that this function mutates given `pipelineJson`
8091
7771
  */
8092
7772
  $applyToPipelineJson(command, $pipelineJson) {
8093
7773
  // TODO: Warn if the version is overridden
@@ -9170,63 +8850,156 @@
9170
8850
  };
9171
8851
 
9172
8852
  /**
9173
- * Constant for model variants.
9174
- *
9175
- * @see {@link ModelVariant}
9176
- *
9177
- * @public exported from `@promptbook/core`
9178
- */
9179
- const MODEL_VARIANTS = [
9180
- 'COMPLETION',
9181
- 'CHAT',
9182
- 'IMAGE_GENERATION',
9183
- 'EMBEDDING' /* <- TODO [🏳] */ /* <- [🤖] */,
9184
- ];
9185
-
9186
- /**
9187
- * Parses the model command
8853
+ * Parses the knowledge command
9188
8854
  *
9189
8855
  * @see `documentationUrl` for more details
9190
- * @deprecated Option to manually set the model requirements is not recommended to use, use `PERSONA` instead
9191
8856
  *
9192
8857
  * @public exported from `@promptbook/editable`
9193
8858
  */
9194
- const modelCommandParser = {
8859
+ const knowledgeCommandParser = {
9195
8860
  /**
9196
8861
  * Name of the command
9197
8862
  */
9198
- name: 'MODEL',
8863
+ name: 'KNOWLEDGE',
9199
8864
  /**
9200
8865
  * BOILERPLATE command can be used in:
9201
8866
  */
9202
8867
  isUsedInPipelineHead: true,
9203
- isUsedInPipelineTask: true,
8868
+ isUsedInPipelineTask: false,
9204
8869
  /**
9205
- * Description of the MODEL command
8870
+ * Description of the KNOWLEDGE command
9206
8871
  */
9207
- description: `Tells which \`modelRequirements\` (for example which model) to use for the prompt task execution`,
8872
+ description: `Tells promptbook which external knowledge to use`,
9208
8873
  /**
9209
8874
  * Link to documentation
9210
8875
  */
9211
- documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/67',
8876
+ documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/41',
9212
8877
  /**
9213
- * Example usages of the MODEL command
8878
+ * Example usages of the KNOWLEDGE command
9214
8879
  */
9215
- examples: ['MODEL VARIANT Chat', 'MODEL NAME `gpt-4`'],
8880
+ examples: [
8881
+ 'KNOWLEDGE https://www.pavolhejny.com/',
8882
+ 'KNOWLEDGE ./hejny-cv.txt',
8883
+ 'KNOWLEDGE ./hejny-cv.md',
8884
+ 'KNOWLEDGE ./hejny-cv.pdf',
8885
+ 'KNOWLEDGE ./hejny-cv.docx',
8886
+ // <- TODO: [😿] Allow ONLY files scoped in the (sub)directory NOT ../ and test it
8887
+ ],
9216
8888
  /**
9217
- * Parses the MODEL command
8889
+ * Parses the KNOWLEDGE command
9218
8890
  */
9219
8891
  parse(input) {
9220
- const { args, normalized } = input;
9221
- const availableVariantsMessage = _spaceTrim.spaceTrim((block) => `
9222
- Available variants are:
9223
- ${block(MODEL_VARIANTS.map((variantName) => `- ${variantName}${variantName !== 'EMBEDDING' ? '' : ' (Not available in pipeline)'}`).join('\n'))}
9224
- `);
9225
- // TODO: Make this more elegant and dynamically
9226
- if (normalized.startsWith('MODEL_VARIANT')) {
9227
- if (normalized === 'MODEL_VARIANT_CHAT') {
9228
- return {
9229
- type: 'MODEL',
8892
+ const { args } = input;
8893
+ const knowledgeSourceContent = _spaceTrim.spaceTrim(args[0] || '');
8894
+ if (knowledgeSourceContent === '') {
8895
+ throw new ParseError(`Source is not defined`);
8896
+ }
8897
+ // TODO: [main] !!4 Following checks should be applied every link in the `sourceContent`
8898
+ if (knowledgeSourceContent.startsWith('http://')) {
8899
+ throw new ParseError(`Source is not secure`);
8900
+ }
8901
+ if (!(isValidFilePath(knowledgeSourceContent) || isValidUrl(knowledgeSourceContent))) {
8902
+ throw new ParseError(`Source not valid`);
8903
+ }
8904
+ if (knowledgeSourceContent.startsWith('../') ||
8905
+ knowledgeSourceContent.startsWith('/') ||
8906
+ /^[A-Z]:[\\/]+/i.test(knowledgeSourceContent)) {
8907
+ throw new ParseError(`Source cannot be outside of the .book.md folder`);
8908
+ }
8909
+ return {
8910
+ type: 'KNOWLEDGE',
8911
+ knowledgeSourceContent,
8912
+ };
8913
+ },
8914
+ /**
8915
+ * Apply the KNOWLEDGE command to the `pipelineJson`
8916
+ *
8917
+ * Note: `$` is used to indicate that this function mutates given `pipelineJson`
8918
+ */
8919
+ $applyToPipelineJson(command, $pipelineJson) {
8920
+ const { knowledgeSourceContent } = command;
8921
+ $pipelineJson.knowledgeSources.push({
8922
+ name: knowledgeSourceContentToName(knowledgeSourceContent),
8923
+ knowledgeSourceContent,
8924
+ });
8925
+ },
8926
+ /**
8927
+ * Converts the KNOWLEDGE command back to string
8928
+ *
8929
+ * Note: This is used in `pipelineJsonToString` utility
8930
+ */
8931
+ stringify(command) {
8932
+ return `---`; // <- TODO: [🛋] Implement
8933
+ },
8934
+ /**
8935
+ * Reads the KNOWLEDGE command from the `PipelineJson`
8936
+ *
8937
+ * Note: This is used in `pipelineJsonToString` utility
8938
+ */
8939
+ takeFromPipelineJson(pipelineJson) {
8940
+ throw new NotYetImplementedError(`[🛋] Not implemented yet`); // <- TODO: [🛋] Implement
8941
+ },
8942
+ };
8943
+ // Note: [⛱] There are two types of KNOWLEDGE commands *...(read more in [⛱])*
8944
+
8945
+ /**
8946
+ * Constant for model variants.
8947
+ *
8948
+ * @see {@link ModelVariant}
8949
+ *
8950
+ * @public exported from `@promptbook/core`
8951
+ */
8952
+ const MODEL_VARIANTS = [
8953
+ 'COMPLETION',
8954
+ 'CHAT',
8955
+ 'IMAGE_GENERATION',
8956
+ 'EMBEDDING' /* <- TODO [🏳] */ /* <- [🤖] */,
8957
+ ];
8958
+
8959
+ /**
8960
+ * Parses the model command
8961
+ *
8962
+ * @see `documentationUrl` for more details
8963
+ * @deprecated Option to manually set the model requirements is not recommended to use, use `PERSONA` instead
8964
+ *
8965
+ * @public exported from `@promptbook/editable`
8966
+ */
8967
+ const modelCommandParser = {
8968
+ /**
8969
+ * Name of the command
8970
+ */
8971
+ name: 'MODEL',
8972
+ /**
8973
+ * BOILERPLATE command can be used in:
8974
+ */
8975
+ isUsedInPipelineHead: true,
8976
+ isUsedInPipelineTask: true,
8977
+ /**
8978
+ * Description of the MODEL command
8979
+ */
8980
+ description: `Tells which \`modelRequirements\` (for example which model) to use for the prompt task execution`,
8981
+ /**
8982
+ * Link to documentation
8983
+ */
8984
+ documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/67',
8985
+ /**
8986
+ * Example usages of the MODEL command
8987
+ */
8988
+ examples: ['MODEL VARIANT Chat', 'MODEL NAME `gpt-4`'],
8989
+ /**
8990
+ * Parses the MODEL command
8991
+ */
8992
+ parse(input) {
8993
+ const { args, normalized } = input;
8994
+ const availableVariantsMessage = _spaceTrim.spaceTrim((block) => `
8995
+ Available variants are:
8996
+ ${block(MODEL_VARIANTS.map((variantName) => `- ${variantName}${variantName !== 'EMBEDDING' ? '' : ' (Not available in pipeline)'}`).join('\n'))}
8997
+ `);
8998
+ // TODO: Make this more elegant and dynamically
8999
+ if (normalized.startsWith('MODEL_VARIANT')) {
9000
+ if (normalized === 'MODEL_VARIANT_CHAT') {
9001
+ return {
9002
+ type: 'MODEL',
9230
9003
  key: 'modelVariant',
9231
9004
  value: 'CHAT',
9232
9005
  };
@@ -9703,69 +9476,296 @@
9703
9476
  };
9704
9477
 
9705
9478
  /**
9706
- * Parses the url command
9479
+ * All available task types
9480
+ *
9481
+ * There is is distinction between task types and section types
9482
+ * - Every section in markdown has its SectionType
9483
+ * - Some sections are tasks but other can be non-task sections
9484
+ *
9485
+ * @public exported from `@promptbook/core`
9486
+ */
9487
+ const TaskTypes = [
9488
+ 'PROMPT',
9489
+ 'SIMPLE',
9490
+ 'SCRIPT',
9491
+ 'DIALOG',
9492
+ // <- [🅱]
9493
+ ];
9494
+
9495
+ /**
9496
+ * All available sections which are not tasks
9497
+ *
9498
+ * @public exported from `@promptbook/core`
9499
+ */
9500
+ const NonTaskSectionTypes = ['EXAMPLE', 'KNOWLEDGE', 'INSTRUMENT', 'ACTION'];
9501
+ /**
9502
+ * All available section types
9503
+ *
9504
+ * There is is distinction between task types and section types
9505
+ * - Every section in markdown has its SectionType
9506
+ * - Some sections are tasks but other can be non-task sections
9507
+ *
9508
+ * @public exported from `@promptbook/core`
9509
+ */
9510
+ const SectionTypes = [
9511
+ ...TaskTypes.map((TaskType) => `${TaskType}_TASK`),
9512
+ ...NonTaskSectionTypes,
9513
+ ];
9514
+
9515
+ /**
9516
+ * Parses the section command
9707
9517
  *
9708
9518
  * @see `documentationUrl` for more details
9709
9519
  *
9710
9520
  * @public exported from `@promptbook/editable`
9711
9521
  */
9712
- const urlCommandParser = {
9522
+ const sectionCommandParser = {
9713
9523
  /**
9714
9524
  * Name of the command
9715
9525
  */
9716
- name: 'URL',
9717
- aliasNames: ['PIPELINE_URL'],
9718
- /*
9719
- Note: [🛵] No need for this alias name because it is already preprocessed
9720
- aliasNames: ['HTTPS'],
9721
- */
9526
+ name: 'SECTION',
9527
+ /**
9528
+ * Aliases for the SECTION command
9529
+ */
9530
+ aliasNames: [
9531
+ 'PROMPT',
9532
+ 'SIMPLE',
9533
+ 'SCRIPT',
9534
+ 'DIALOG',
9535
+ 'SAMPLE',
9536
+ 'EXAMPLE',
9537
+ 'KNOWLEDGE',
9538
+ 'INSTRUMENT',
9539
+ 'ACTION', // <- Note: [⛱]
9540
+ ],
9541
+ /**
9542
+ * Aliases for the SECTION command
9543
+ */
9544
+ deprecatedNames: ['TEMPLATE', 'BLOCK', 'EXECUTE'],
9722
9545
  /**
9723
9546
  * BOILERPLATE command can be used in:
9724
9547
  */
9725
- isUsedInPipelineHead: true,
9726
- isUsedInPipelineTask: false,
9548
+ isUsedInPipelineHead: false,
9549
+ isUsedInPipelineTask: true,
9727
9550
  /**
9728
- * Description of the URL command
9551
+ * Description of the SECTION command
9729
9552
  */
9730
- description: `Declares unique URL for the pipeline`,
9553
+ description: `Defines the purpose of the markdown section - if its a task and which type or something else`,
9731
9554
  /**
9732
9555
  * Link to documentation
9733
9556
  */
9734
- documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/70',
9557
+ documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/64',
9735
9558
  /**
9736
- * Example usages of the URL command
9559
+ * Example usages of the SECTION command
9737
9560
  */
9738
9561
  examples: [
9739
- 'PIPELINE URL https://promptbook.studio/library/write-cv.book',
9740
- 'URL https://promptbook.studio/library/write-cv.book',
9741
- 'https://promptbook.studio/library/write-cv.book',
9562
+ // Short form:
9563
+ 'PROMPT',
9564
+ 'SIMPLE',
9565
+ 'SCRIPT',
9566
+ 'DIALOG',
9567
+ // <- [🅱]
9568
+ 'EXAMPLE',
9569
+ 'KNOWLEDGE',
9570
+ 'INSTRUMENT',
9571
+ 'ACTION',
9572
+ // -----------------
9573
+ // Recommended (reversed) form:
9574
+ 'PROMPT SECTION',
9575
+ 'SIMPLE SECTION',
9576
+ 'SCRIPT SECTION',
9577
+ 'DIALOG SECTION',
9578
+ // <- [🅱]
9579
+ 'EXAMPLE SECTION',
9580
+ 'KNOWLEDGE SECTION',
9581
+ 'INSTRUMENT SECTION',
9582
+ 'ACTION SECTION',
9583
+ // -----------------
9584
+ // Standard form:
9585
+ 'SECTION PROMPT',
9586
+ 'SECTION SIMPLE',
9587
+ 'SECTION SCRIPT',
9588
+ 'SECTION DIALOG',
9589
+ // <- [🅱]
9590
+ 'SECTION EXAMPLE',
9591
+ 'SECTION KNOWLEDGE',
9592
+ 'SECTION INSTRUMENT',
9593
+ 'SECTION ACTION',
9742
9594
  ],
9595
+ // TODO: [♓️] order: -10 /* <- Note: Putting before other commands */
9743
9596
  /**
9744
- * Parses the URL command
9597
+ * Parses the SECTION command
9745
9598
  */
9746
9599
  parse(input) {
9747
- const { args } = input;
9748
- const pipelineUrl = args.pop();
9749
- if (pipelineUrl === undefined) {
9750
- throw new ParseError(`URL is required`);
9751
- }
9752
- // TODO: [🧠][🚲] This should be maybe tested as logic not parse
9753
- if (!isValidPipelineUrl(pipelineUrl)) {
9754
- throw new ParseError(`Invalid pipeline URL "${pipelineUrl}"`);
9755
- }
9756
- if (args.length > 0) {
9757
- throw new ParseError(`Can not have more than one pipeline URL`);
9758
- }
9759
- /*
9760
- TODO: [🐠 Maybe more info from `isValidPipelineUrl`:
9761
- if (pipelineUrl.protocol !== 'https:') {
9762
- throw new ParseError(`Protocol must be HTTPS`);
9763
- }
9600
+ let { normalized } = input;
9601
+ normalized = normalized.split('SAMPLE').join('EXAMPLE');
9602
+ normalized = normalized.split('EXECUTE_').join('');
9603
+ normalized = normalized.split('DIALOGUE').join('DIALOG');
9604
+ const taskTypes = SectionTypes.filter((sectionType) => normalized.includes(sectionType.split('_TASK').join('')));
9605
+ if (taskTypes.length !== 1) {
9606
+ throw new ParseError(_spaceTrim.spaceTrim((block) => `
9607
+ Unknown section type "${normalized}"
9764
9608
 
9765
- if (pipelineUrl.hash !== '') {
9766
- throw new ParseError(
9767
- spaceTrim(
9768
- `
9609
+ Supported section types are:
9610
+ ${block(SectionTypes.join(', '))}
9611
+ `));
9612
+ }
9613
+ const taskType = taskTypes[0];
9614
+ return {
9615
+ type: 'SECTION',
9616
+ taskType,
9617
+ };
9618
+ },
9619
+ /**
9620
+ * Apply the SECTION command to the `pipelineJson`
9621
+ *
9622
+ * Note: `$` is used to indicate that this function mutates given `taskJson`
9623
+ */
9624
+ $applyToTaskJson(command, $taskJson, $pipelineJson) {
9625
+ if ($taskJson.isSectionTypeSet === true) {
9626
+ throw new ParseError(_spaceTrim.spaceTrim(`
9627
+ Section type is already defined in the section.
9628
+ It can be defined only once.
9629
+ `));
9630
+ }
9631
+ $taskJson.isSectionTypeSet = true;
9632
+ // TODO: [🍧][💩] Rearrange better - but at bottom and unwrap from function
9633
+ const expectResultingParameterName = () => {
9634
+ if ($taskJson.resultingParameterName) {
9635
+ return;
9636
+ }
9637
+ throw new ParseError(`Task section and example section must end with return statement -> {parameterName}`);
9638
+ };
9639
+ if ($taskJson.content === undefined) {
9640
+ throw new UnexpectedError(`Content is missing in the taskJson - probably commands are applied in wrong order`);
9641
+ }
9642
+ if (command.taskType === 'EXAMPLE') {
9643
+ expectResultingParameterName();
9644
+ const parameter = $pipelineJson.parameters.find((param) => param.name === $taskJson.resultingParameterName);
9645
+ if (parameter === undefined) {
9646
+ // TODO: !!6 Change to logic error for higher level abstraction of chatbot to work
9647
+ throw new ParseError(`Parameter \`{${$taskJson.resultingParameterName}}\` is not defined so can not define example value of it`);
9648
+ }
9649
+ parameter.exampleValues = parameter.exampleValues || [];
9650
+ parameter.exampleValues.push($taskJson.content);
9651
+ $taskJson.isTask = false;
9652
+ return;
9653
+ }
9654
+ if (command.taskType === 'KNOWLEDGE') {
9655
+ knowledgeCommandParser.$applyToPipelineJson({
9656
+ type: 'KNOWLEDGE',
9657
+ knowledgeSourceContent: $taskJson.content, // <- TODO: [🐝][main] !!3 Work with KNOWLEDGE which not referring to the source file or website, but its content itself
9658
+ }, $pipelineJson);
9659
+ $taskJson.isTask = false;
9660
+ return;
9661
+ }
9662
+ if (command.taskType === 'ACTION') {
9663
+ console.error(new NotYetImplementedError('Actions are not implemented yet'));
9664
+ $taskJson.isTask = false;
9665
+ return;
9666
+ }
9667
+ if (command.taskType === 'INSTRUMENT') {
9668
+ console.error(new NotYetImplementedError('Instruments are not implemented yet'));
9669
+ $taskJson.isTask = false;
9670
+ return;
9671
+ }
9672
+ expectResultingParameterName();
9673
+ $taskJson.taskType = command.taskType;
9674
+ $taskJson.isTask = true;
9675
+ },
9676
+ /**
9677
+ * Converts the SECTION command back to string
9678
+ *
9679
+ * Note: This is used in `pipelineJsonToString` utility
9680
+ */
9681
+ stringify(command) {
9682
+ return `---`; // <- TODO: [🛋] Implement
9683
+ },
9684
+ /**
9685
+ * Reads the SECTION command from the `TaskJson`
9686
+ *
9687
+ * Note: This is used in `pipelineJsonToString` utility
9688
+ */
9689
+ takeFromTaskJson($taskJson) {
9690
+ throw new NotYetImplementedError(`[🛋] Not implemented yet`); // <- TODO: [🛋] Implement
9691
+ },
9692
+ };
9693
+ /**
9694
+ * Note: [⛱] There are two types of KNOWLEDGE, ACTION and INSTRUMENT commands:
9695
+ * 1) There are commands `KNOWLEDGE`, `ACTION` and `INSTRUMENT` used in the pipeline head, they just define the knowledge, action or instrument as single line after the command
9696
+ * - KNOWLEDGE Look at https://en.wikipedia.org/wiki/Artificial_intelligence
9697
+ * 2) `KNOWLEDGE SECTION` which has short form `KNOWLEDGE` is used in the sectiom, does not refer the line itself, but the content of the section block
9698
+ * - KNOWLEDGE SECTION
9699
+ *
9700
+ * ```
9701
+ * Look at https://en.wikipedia.org/wiki/Artificial_intelligence
9702
+ * ```
9703
+ */
9704
+
9705
+ /**
9706
+ * Parses the url command
9707
+ *
9708
+ * @see `documentationUrl` for more details
9709
+ *
9710
+ * @public exported from `@promptbook/editable`
9711
+ */
9712
+ const urlCommandParser = {
9713
+ /**
9714
+ * Name of the command
9715
+ */
9716
+ name: 'URL',
9717
+ aliasNames: ['PIPELINE_URL'],
9718
+ /*
9719
+ Note: [🛵] No need for this alias name because it is already preprocessed
9720
+ aliasNames: ['HTTPS'],
9721
+ */
9722
+ /**
9723
+ * BOILERPLATE command can be used in:
9724
+ */
9725
+ isUsedInPipelineHead: true,
9726
+ isUsedInPipelineTask: false,
9727
+ /**
9728
+ * Description of the URL command
9729
+ */
9730
+ description: `Declares unique URL for the pipeline`,
9731
+ /**
9732
+ * Link to documentation
9733
+ */
9734
+ documentationUrl: 'https://github.com/webgptorg/promptbook/discussions/70',
9735
+ /**
9736
+ * Example usages of the URL command
9737
+ */
9738
+ examples: [
9739
+ 'PIPELINE URL https://promptbook.studio/library/write-cv.book',
9740
+ 'URL https://promptbook.studio/library/write-cv.book',
9741
+ 'https://promptbook.studio/library/write-cv.book',
9742
+ ],
9743
+ /**
9744
+ * Parses the URL command
9745
+ */
9746
+ parse(input) {
9747
+ const { args } = input;
9748
+ const pipelineUrl = args.pop();
9749
+ if (pipelineUrl === undefined) {
9750
+ throw new ParseError(`URL is required`);
9751
+ }
9752
+ // TODO: [🧠][🚲] This should be maybe tested as logic not parse
9753
+ if (!isValidPipelineUrl(pipelineUrl)) {
9754
+ throw new ParseError(`Invalid pipeline URL "${pipelineUrl}"`);
9755
+ }
9756
+ if (args.length > 0) {
9757
+ throw new ParseError(`Can not have more than one pipeline URL`);
9758
+ }
9759
+ /*
9760
+ TODO: [🐠 Maybe more info from `isValidPipelineUrl`:
9761
+ if (pipelineUrl.protocol !== 'https:') {
9762
+ throw new ParseError(`Protocol must be HTTPS`);
9763
+ }
9764
+
9765
+ if (pipelineUrl.hash !== '') {
9766
+ throw new ParseError(
9767
+ spaceTrim(
9768
+ `
9769
9769
  URL must not contain hash
9770
9770
  Hash is used for identification of the section of the pipeline
9771
9771
  `,
@@ -10156,85 +10156,321 @@
10156
10156
  }
10157
10157
 
10158
10158
  /**
10159
- * Extracts the interface (input and output parameters) from a pipeline.
10159
+ * Utility function to extract all list items from markdown
10160
10160
  *
10161
- * @deprecated https://github.com/webgptorg/promptbook/pull/186
10162
- * @see https://github.com/webgptorg/promptbook/discussions/171
10161
+ * Note: It works with both ul and ol
10162
+ * Note: It omits list items in code blocks
10163
+ * Note: It flattens nested lists
10164
+ * Note: It can not work with html syntax and comments
10163
10165
  *
10164
- * @public exported from `@promptbook/core`
10166
+ * @param markdown any valid markdown
10167
+ * @returns An array of strings, each representing an individual list item found in the markdown
10168
+ *
10169
+ * @public exported from `@promptbook/markdown-utils`
10165
10170
  */
10166
- function getPipelineInterface(pipeline) {
10167
- const pipelineInterface = {
10168
- inputParameters: [],
10169
- outputParameters: [],
10170
- };
10171
- for (const parameter of pipeline.parameters) {
10172
- const { isInput, isOutput } = parameter;
10173
- if (isInput) {
10174
- pipelineInterface.inputParameters.push(deepClone(parameter));
10171
+ function extractAllListItemsFromMarkdown(markdown) {
10172
+ const lines = markdown.split(/\r?\n/);
10173
+ const listItems = [];
10174
+ let isInCodeBlock = false;
10175
+ for (const line of lines) {
10176
+ const trimmedLine = line.trim();
10177
+ if (trimmedLine.startsWith('```')) {
10178
+ isInCodeBlock = !isInCodeBlock;
10175
10179
  }
10176
- if (isOutput) {
10177
- pipelineInterface.outputParameters.push(deepClone(parameter));
10180
+ if (!isInCodeBlock && (trimmedLine.startsWith('-') || trimmedLine.match(/^\d+\./))) {
10181
+ const listItem = trimmedLine.replace(/^-|\d+\./, '').trim();
10182
+ listItems.push(listItem);
10178
10183
  }
10179
10184
  }
10180
- for (const key of ['inputParameters', 'outputParameters']) {
10181
- pipelineInterface[key].sort(({ name: name1 }, { name: name2 }) => name1.localeCompare(name2));
10182
- }
10183
- return exportJson({
10184
- name: `pipelineInterface`,
10185
- message: `Result of \`getPipelineInterface\``,
10186
- order: ['inputParameters', 'outputParameters'],
10187
- value: pipelineInterface,
10188
- });
10185
+ return listItems;
10189
10186
  }
10190
10187
 
10191
10188
  /**
10192
- * Checks if two pipeline interfaces are structurally identical.
10193
- *
10194
- * @deprecated https://github.com/webgptorg/promptbook/pull/186
10195
- * @see https://github.com/webgptorg/promptbook/discussions/171
10189
+ * Builds a short file/url identification block for parse errors.
10196
10190
  *
10197
- * @public exported from `@promptbook/core`
10191
+ * @private internal utility of `parsePipeline`
10198
10192
  */
10199
- function isPipelineInterfacesEqual(pipelineInterface1, pipelineInterface2) {
10200
- for (const whichParameters of ['inputParameters', 'outputParameters']) {
10201
- const parameters1 = pipelineInterface1[whichParameters]; // <- Note: `isPipelineInterfacesEqual` is just temporary solution, no need to fix this
10202
- const parameters2 = pipelineInterface2[whichParameters];
10203
- if (parameters1.length !== parameters2.length) {
10204
- return false;
10205
- }
10206
- for (const parameter of parameters1) {
10207
- const matchingParameter = parameters2.find(({ name }) => name === parameter.name);
10208
- if (!matchingParameter) {
10209
- return false;
10210
- }
10211
- // Note: Do not compare description, it is not relevant for compatibility
10212
- if (matchingParameter.isInput !== parameter.isInput) {
10213
- return false;
10214
- }
10215
- if (matchingParameter.isOutput !== parameter.isOutput) {
10216
- return false;
10217
- }
10218
- }
10193
+ function getPipelineIdentification($pipelineJson) {
10194
+ // Note: This is a 😐 implementation of [🚞]
10195
+ const pipelineIdentificationParts = [];
10196
+ if ($pipelineJson.sourceFile !== undefined) {
10197
+ pipelineIdentificationParts.push(`File: ${$pipelineJson.sourceFile}`);
10219
10198
  }
10220
- return true;
10199
+ if ($pipelineJson.pipelineUrl !== undefined) {
10200
+ pipelineIdentificationParts.push(`Url: ${$pipelineJson.pipelineUrl}`);
10201
+ }
10202
+ return pipelineIdentificationParts.join('\n');
10221
10203
  }
10222
10204
 
10223
10205
  /**
10224
- * Checks if a given pipeline satisfies the requirements of a specified pipeline interface.
10225
- *
10226
- * @deprecated https://github.com/webgptorg/promptbook/pull/186
10227
- * @see https://github.com/webgptorg/promptbook/discussions/171
10228
- * @returns `true` if the pipeline implements the interface, `false` otherwise.
10206
+ * Merges one parameter declaration into the mutable pipeline parameter list.
10229
10207
  *
10230
- * @public exported from `@promptbook/core`
10208
+ * @private internal utility of `parsePipeline`
10231
10209
  */
10232
- function isPipelineImplementingInterface(options) {
10233
- const { pipeline, pipelineInterface } = options;
10234
- return isPipelineInterfacesEqual(getPipelineInterface(pipeline), pipelineInterface);
10235
- }
10236
-
10237
- /**
10210
+ function defineParameter($pipelineJson, parameterCommand) {
10211
+ const { parameterName, parameterDescription, isInput, isOutput } = parameterCommand;
10212
+ if (RESERVED_PARAMETER_NAMES.includes(parameterName)) {
10213
+ throw new ParseError(_spaceTrim.spaceTrim((block) => `
10214
+ Parameter name {${parameterName}} is reserved and cannot be used as resulting parameter name
10215
+
10216
+ ${block(getPipelineIdentification($pipelineJson))}
10217
+ `) /* <- TODO: [🚞] */);
10218
+ }
10219
+ const existingParameter = $pipelineJson.parameters.find((parameter) => parameter.name === parameterName);
10220
+ if (existingParameter &&
10221
+ existingParameter.description &&
10222
+ existingParameter.description !== parameterDescription &&
10223
+ parameterDescription) {
10224
+ throw new ParseError(_spaceTrim.spaceTrim((block) => `
10225
+ Parameter \`{${parameterName}}\` is defined multiple times with different description:
10226
+
10227
+ ${block(getPipelineIdentification($pipelineJson))}
10228
+
10229
+ First definition:
10230
+ ${block(existingParameter.description || '[undefined]')}
10231
+
10232
+ Second definition:
10233
+ ${block(parameterDescription || '[undefined]')}
10234
+ `));
10235
+ }
10236
+ if (existingParameter) {
10237
+ if (parameterDescription) {
10238
+ existingParameter.description = parameterDescription;
10239
+ }
10240
+ existingParameter.isInput = existingParameter.isInput || isInput;
10241
+ existingParameter.isOutput = existingParameter.isOutput || isOutput;
10242
+ return;
10243
+ }
10244
+ $pipelineJson.parameters.push({
10245
+ name: parameterName,
10246
+ description: parameterDescription || undefined,
10247
+ isInput,
10248
+ isOutput,
10249
+ });
10250
+ }
10251
+
10252
+ /**
10253
+ * Removes fenced code blocks when deriving human-readable section descriptions.
10254
+ *
10255
+ * @private internal utility of `extractPipelineDescription`
10256
+ */
10257
+ const DESCRIPTION_CODE_BLOCK_REGEXP = /^```.*^```/gms;
10258
+ /**
10259
+ * Removes blockquote lines when deriving human-readable section descriptions.
10260
+ *
10261
+ * @private internal utility of `extractPipelineDescription`
10262
+ */
10263
+ const DESCRIPTION_BLOCKQUOTE_REGEXP = /^>.*$/gm;
10264
+ /**
10265
+ * Removes list items and return statements when deriving human-readable section descriptions.
10266
+ *
10267
+ * @private internal utility of `extractPipelineDescription`
10268
+ */
10269
+ const DESCRIPTION_LIST_ITEM_REGEXP = /^(?:(?:-)|(?:\d\))|(?:`?->))\s+.*$/gm;
10270
+ /**
10271
+ * Extracts the plain-text description from a head or task section body.
10272
+ *
10273
+ * @private internal utility of `parsePipeline`
10274
+ */
10275
+ function extractPipelineDescription(sectionContent) {
10276
+ let description = sectionContent;
10277
+ description = description.split(DESCRIPTION_CODE_BLOCK_REGEXP).join('');
10278
+ description = description.split(DESCRIPTION_BLOCKQUOTE_REGEXP).join('');
10279
+ description = description.split(DESCRIPTION_LIST_ITEM_REGEXP).join('');
10280
+ description = _spaceTrim.spaceTrim(description);
10281
+ if (description === '') {
10282
+ return undefined;
10283
+ }
10284
+ return description;
10285
+ }
10286
+
10287
+ /**
10288
+ * Applies the pipeline head title, description, and head-level commands.
10289
+ *
10290
+ * @private internal utility of `parsePipeline`
10291
+ */
10292
+ function applyPipelineHead(pipelineHead, $pipelineJson) {
10293
+ $pipelineJson.title = pipelineHead.title;
10294
+ $pipelineJson.description = extractPipelineDescription(pipelineHead.content);
10295
+ for (const listItem of extractAllListItemsFromMarkdown(pipelineHead.content)) {
10296
+ applyPipelineHeadCommand(listItem, $pipelineJson);
10297
+ }
10298
+ }
10299
+ /**
10300
+ * Parses and applies one command declared in the pipeline head.
10301
+ *
10302
+ * @private internal utility of `applyPipelineHead`
10303
+ */
10304
+ function applyPipelineHeadCommand(listItem, $pipelineJson) {
10305
+ const command = parseCommand(listItem, 'PIPELINE_HEAD');
10306
+ const commandParser = getParserForCommand(command);
10307
+ if (commandParser.isUsedInPipelineHead !== true /* <- Note: [🦦][4] */) {
10308
+ throw new ParseError(_spaceTrim.spaceTrim((block) => `
10309
+ Command \`${command.type}\` is not allowed in the head of the pipeline ONLY at the pipeline task
10310
+
10311
+ ${block(getPipelineIdentification($pipelineJson))}
10312
+ `)); // <- TODO: [🚞]
10313
+ }
10314
+ try {
10315
+ commandParser.$applyToPipelineJson(command, $pipelineJson);
10316
+ // <- Note: [🦦] Its strange that this assertion must be here, [🦦][4] should do this assertion implicitly
10317
+ }
10318
+ catch (error) {
10319
+ if (!(error instanceof ParseError)) {
10320
+ throw error;
10321
+ }
10322
+ throw new ParseError(_spaceTrim.spaceTrim((block) => `
10323
+ Command ${command.type} failed to apply to the pipeline
10324
+
10325
+ The error:
10326
+ ${block(error.message)}
10327
+
10328
+ Raw command:
10329
+ - ${listItem}
10330
+
10331
+ Usage of ${command.type}:
10332
+ ${block(commandParser.examples.map((example) => `- ${example}`).join('\n'))}
10333
+
10334
+ ${block(getPipelineIdentification($pipelineJson))}
10335
+ `)); // <- TODO: [🚞]
10336
+ }
10337
+ if (command.type === 'PARAMETER') {
10338
+ defineParameter($pipelineJson, command);
10339
+ // <- Note: [🍣]
10340
+ }
10341
+ }
10342
+
10343
+ /**
10344
+ * Creates the mutable pipeline JSON structure used throughout parsing.
10345
+ *
10346
+ * @private internal utility of `parsePipeline`
10347
+ */
10348
+ function createInitialPipelineJson(pipelineString) {
10349
+ return {
10350
+ title: DEFAULT_BOOK_TITLE,
10351
+ parameters: [],
10352
+ tasks: [],
10353
+ knowledgeSources: [],
10354
+ knowledgePieces: [],
10355
+ personas: [],
10356
+ preparations: [],
10357
+ sources: [
10358
+ {
10359
+ type: 'BOOK',
10360
+ path: null,
10361
+ // <- TODO: !!6 Pass here path of the file
10362
+ content: pipelineString,
10363
+ },
10364
+ ],
10365
+ };
10366
+ }
10367
+
10368
+ /**
10369
+ * Creates stable unique task names for duplicate section titles.
10370
+ *
10371
+ * @private internal utility of `parsePipeline`
10372
+ */
10373
+ function createUniqueSectionNameResolver(pipelineSections) {
10374
+ const sectionCounts = {};
10375
+ for (const pipelineSection of pipelineSections) {
10376
+ const sectionName = titleToName(pipelineSection.title);
10377
+ if (sectionCounts[sectionName] === undefined) {
10378
+ sectionCounts[sectionName] = { count: 0, currentIndex: 0 };
10379
+ }
10380
+ sectionCounts[sectionName].count++;
10381
+ }
10382
+ return (title) => {
10383
+ const sectionName = titleToName(title);
10384
+ const sectionCount = sectionCounts[sectionName];
10385
+ if (sectionCount.count === 1) {
10386
+ return sectionName;
10387
+ }
10388
+ const nameWithSuffix = `${sectionName}-${sectionCount.currentIndex}`;
10389
+ sectionCount.currentIndex++;
10390
+ return nameWithSuffix;
10391
+ };
10392
+ }
10393
+
10394
+ /**
10395
+ * Extracts the interface (input and output parameters) from a pipeline.
10396
+ *
10397
+ * @deprecated https://github.com/webgptorg/promptbook/pull/186
10398
+ * @see https://github.com/webgptorg/promptbook/discussions/171
10399
+ *
10400
+ * @public exported from `@promptbook/core`
10401
+ */
10402
+ function getPipelineInterface(pipeline) {
10403
+ const pipelineInterface = {
10404
+ inputParameters: [],
10405
+ outputParameters: [],
10406
+ };
10407
+ for (const parameter of pipeline.parameters) {
10408
+ const { isInput, isOutput } = parameter;
10409
+ if (isInput) {
10410
+ pipelineInterface.inputParameters.push(deepClone(parameter));
10411
+ }
10412
+ if (isOutput) {
10413
+ pipelineInterface.outputParameters.push(deepClone(parameter));
10414
+ }
10415
+ }
10416
+ for (const key of ['inputParameters', 'outputParameters']) {
10417
+ pipelineInterface[key].sort(({ name: name1 }, { name: name2 }) => name1.localeCompare(name2));
10418
+ }
10419
+ return exportJson({
10420
+ name: `pipelineInterface`,
10421
+ message: `Result of \`getPipelineInterface\``,
10422
+ order: ['inputParameters', 'outputParameters'],
10423
+ value: pipelineInterface,
10424
+ });
10425
+ }
10426
+
10427
+ /**
10428
+ * Checks if two pipeline interfaces are structurally identical.
10429
+ *
10430
+ * @deprecated https://github.com/webgptorg/promptbook/pull/186
10431
+ * @see https://github.com/webgptorg/promptbook/discussions/171
10432
+ *
10433
+ * @public exported from `@promptbook/core`
10434
+ */
10435
+ function isPipelineInterfacesEqual(pipelineInterface1, pipelineInterface2) {
10436
+ for (const whichParameters of ['inputParameters', 'outputParameters']) {
10437
+ const parameters1 = pipelineInterface1[whichParameters]; // <- Note: `isPipelineInterfacesEqual` is just temporary solution, no need to fix this
10438
+ const parameters2 = pipelineInterface2[whichParameters];
10439
+ if (parameters1.length !== parameters2.length) {
10440
+ return false;
10441
+ }
10442
+ for (const parameter of parameters1) {
10443
+ const matchingParameter = parameters2.find(({ name }) => name === parameter.name);
10444
+ if (!matchingParameter) {
10445
+ return false;
10446
+ }
10447
+ // Note: Do not compare description, it is not relevant for compatibility
10448
+ if (matchingParameter.isInput !== parameter.isInput) {
10449
+ return false;
10450
+ }
10451
+ if (matchingParameter.isOutput !== parameter.isOutput) {
10452
+ return false;
10453
+ }
10454
+ }
10455
+ }
10456
+ return true;
10457
+ }
10458
+
10459
+ /**
10460
+ * Checks if a given pipeline satisfies the requirements of a specified pipeline interface.
10461
+ *
10462
+ * @deprecated https://github.com/webgptorg/promptbook/pull/186
10463
+ * @see https://github.com/webgptorg/promptbook/discussions/171
10464
+ * @returns `true` if the pipeline implements the interface, `false` otherwise.
10465
+ *
10466
+ * @public exported from `@promptbook/core`
10467
+ */
10468
+ function isPipelineImplementingInterface(options) {
10469
+ const { pipeline, pipelineInterface } = options;
10470
+ return isPipelineInterfacesEqual(getPipelineInterface(pipeline), pipelineInterface);
10471
+ }
10472
+
10473
+ /**
10238
10474
  * Set formfactor based on the pipeline interface e
10239
10475
  *
10240
10476
  * @private
@@ -10416,197 +10652,125 @@
10416
10652
  // Note: [💞] Ignore a discrepancy between file name and entity name
10417
10653
 
10418
10654
  /**
10419
- * Supported script languages
10655
+ * Applies postprocessing and exports the parsed pipeline JSON.
10420
10656
  *
10421
- * @private internal base for `ScriptLanguage`
10657
+ * @private internal utility of `parsePipeline`
10422
10658
  */
10423
- const SUPPORTED_SCRIPT_LANGUAGES = ['javascript', 'typescript', 'python'];
10424
- // <- TODO: [🏥] DRY
10425
-
10659
+ function finalizeParsedPipeline($pipelineJson) {
10660
+ applyImplicitParameterDirections($pipelineJson);
10661
+ removeUndefinedValuesFromPipeline($pipelineJson);
10662
+ applySyncHighLevelAbstractions($pipelineJson);
10663
+ ensurePipelineFormfactor($pipelineJson);
10664
+ return exportParsedPipelineJson($pipelineJson);
10665
+ }
10426
10666
  /**
10427
- * Number of padding lines to add at the end of the book content
10667
+ * Applies default INPUT/OUTPUT flags when the author did not specify them explicitly.
10428
10668
  *
10429
- * @public exported from `@promptbook/core`
10669
+ * @private internal utility of `finalizeParsedPipeline`
10430
10670
  */
10431
- const PADDING_LINES = 11;
10671
+ function applyImplicitParameterDirections($pipelineJson) {
10672
+ markImplicitInputParameters($pipelineJson);
10673
+ markImplicitOutputParameters($pipelineJson);
10674
+ }
10432
10675
  /**
10433
- * A function that adds padding to the book content
10434
- *
10435
- * Note: [🔂] This function is idempotent.
10676
+ * Marks non-result parameters as pipeline inputs when no input was declared.
10436
10677
  *
10437
- * @public exported from `@promptbook/core`
10678
+ * @private internal utility of `finalizeParsedPipeline`
10438
10679
  */
10439
- function padBook(content) {
10440
- if (!content) {
10441
- return '\n'.repeat(PADDING_LINES);
10680
+ function markImplicitInputParameters($pipelineJson) {
10681
+ if ($pipelineJson.parameters.some((parameter) => parameter.isInput)) {
10682
+ return;
10442
10683
  }
10443
- const lines = content.split(/\r?\n/);
10444
- let trailingEmptyLines = 0;
10445
- for (let i = lines.length - 1; i >= 0; i--) {
10446
- const line = lines[i];
10447
- if (line === undefined) {
10448
- // Note: This should not happen in reality, but it's here to satisfy TypeScript's noUncheckedIndexedAccess option
10449
- continue;
10450
- }
10451
- if (line.trim() === '') {
10452
- trailingEmptyLines++;
10453
- }
10454
- else {
10455
- break;
10684
+ for (const parameter of $pipelineJson.parameters) {
10685
+ const isThisParameterResulting = $pipelineJson.tasks.some((task) => task.resultingParameterName === parameter.name);
10686
+ if (!isThisParameterResulting) {
10687
+ parameter.isInput = true;
10688
+ // <- TODO: [💔] Why this is making typescript error in vscode but not in cli
10689
+ // > Type 'true' is not assignable to type 'false'.ts(2322)
10690
+ // > (property) isInput: false
10691
+ // > The parameter is input of the pipeline The parameter is NOT input of the pipeline
10456
10692
  }
10457
10693
  }
10458
- if (trailingEmptyLines >= PADDING_LINES) {
10459
- return content;
10460
- }
10461
- const linesToAdd = PADDING_LINES - trailingEmptyLines;
10462
- return (content + '\n'.repeat(linesToAdd));
10463
10694
  }
10464
- // TODO: [🧠] Maybe export
10465
-
10466
10695
  /**
10467
- * Removes Markdown (or HTML) comments
10468
- *
10469
- * @param {string} content - The string to remove comments from.
10470
- * @returns {string} The input string with all comments removed.
10471
- *
10472
- * @public exported from `@promptbook/markdown-utils`
10473
- */
10474
- function removeMarkdownComments(content) {
10475
- return _spaceTrim.spaceTrim(content.replace(/<!--(.*?)-->/gs, ''));
10476
- }
10477
-
10478
- /**
10479
- * Utility to determine if a pipeline string is in flat format.
10480
- * A flat pipeline is a simple text without proper structure (headers, blocks, etc).
10481
- *
10482
- * @public exported from `@promptbook/editable`
10483
- */
10484
- function isFlatPipeline(pipelineString) {
10485
- pipelineString = removeMarkdownComments(pipelineString);
10486
- pipelineString = _spaceTrim.spaceTrim(pipelineString);
10487
- const isMarkdownBeginningWithHeadline = pipelineString.startsWith('# ');
10488
- //const isLastLineReturnStatement = pipelineString.split(/\r?\n/).pop()!.split('`').join('').startsWith('->');
10489
- const isBacktickBlockUsed = pipelineString.includes('```');
10490
- const isQuoteBlocksUsed = /^>\s+/m.test(pipelineString);
10491
- const isBlocksUsed = isBacktickBlockUsed || isQuoteBlocksUsed;
10492
- // TODO: [🧉] Also (double)check
10493
- // > const usedCommands
10494
- // > const isBlocksUsed
10495
- // > const returnStatementCount
10496
- const isFlat = !isMarkdownBeginningWithHeadline && !isBlocksUsed; /* && isLastLineReturnStatement */
10497
- return isFlat;
10498
- }
10499
-
10500
- /**
10501
- * Converts a pipeline structure to its string representation.
10502
- *
10503
- * Transforms a flat, simple pipeline into a properly formatted pipeline string
10504
- * with sections for title, prompt, and return statement.
10696
+ * Marks every non-input parameter as output when no output was declared.
10505
10697
  *
10506
- * @public exported from `@promptbook/editable`
10698
+ * @private internal utility of `finalizeParsedPipeline`
10507
10699
  */
10508
- function deflatePipeline(pipelineString) {
10509
- if (!isFlatPipeline(pipelineString)) {
10510
- return pipelineString;
10511
- }
10512
- pipelineString = _spaceTrim.spaceTrim(pipelineString);
10513
- const pipelineStringLines = pipelineString.split(/\r?\n/);
10514
- const potentialReturnStatement = pipelineStringLines.pop();
10515
- let returnStatement;
10516
- if (/(-|=)>\s*\{.*\}/.test(potentialReturnStatement)) {
10517
- // Note: Last line is return statement
10518
- returnStatement = potentialReturnStatement;
10519
- }
10520
- else {
10521
- // Note: Last line is not a return statement
10522
- returnStatement = `-> {${DEFAULT_BOOK_OUTPUT_PARAMETER_NAME}}`;
10523
- pipelineStringLines.push(potentialReturnStatement);
10524
- }
10525
- const prompt = _spaceTrim.spaceTrim(pipelineStringLines.join('\n'));
10526
- let quotedPrompt;
10527
- if (prompt.split(/\r?\n/).length <= 1) {
10528
- quotedPrompt = `> ${prompt}`;
10529
- }
10530
- else {
10531
- quotedPrompt = _spaceTrim.spaceTrim((block) => `
10532
- \`\`\`
10533
- ${block(prompt.split('`').join('\\`'))}
10534
- \`\`\`
10535
- `);
10700
+ function markImplicitOutputParameters($pipelineJson) {
10701
+ if ($pipelineJson.parameters.some((parameter) => parameter.isOutput)) {
10702
+ return;
10703
+ }
10704
+ for (const parameter of $pipelineJson.parameters) {
10705
+ if (!parameter.isInput) {
10706
+ parameter.isOutput = true;
10707
+ // <- TODO: [💔]
10708
+ }
10536
10709
  }
10537
- pipelineString = validatePipelineString(_spaceTrim.spaceTrim((block) => `
10538
- # ${DEFAULT_BOOK_TITLE}
10539
-
10540
- ## Prompt
10541
-
10542
- ${block(quotedPrompt)}
10543
-
10544
- ${returnStatement}
10545
- `));
10546
- // <- TODO: Maybe use book` notation
10547
- return padBook(pipelineString);
10548
10710
  }
10549
- // TODO: Unit test
10550
-
10551
10711
  /**
10552
- * Utility function to extract all list items from markdown
10553
- *
10554
- * Note: It works with both ul and ol
10555
- * Note: It omits list items in code blocks
10556
- * Note: It flattens nested lists
10557
- * Note: It can not work with html syntax and comments
10712
+ * Removes `undefined` properties from serialized tasks and parameters.
10558
10713
  *
10559
- * @param markdown any valid markdown
10560
- * @returns An array of strings, each representing an individual list item found in the markdown
10714
+ * @private internal utility of `finalizeParsedPipeline`
10715
+ */
10716
+ function removeUndefinedValuesFromPipeline($pipelineJson) {
10717
+ $pipelineJson.tasks.forEach(removeUndefinedProperties);
10718
+ $pipelineJson.parameters.forEach(removeUndefinedProperties);
10719
+ }
10720
+ /**
10721
+ * Deletes all own properties with `undefined` values from a mutable JSON entity.
10561
10722
  *
10562
- * @public exported from `@promptbook/markdown-utils`
10723
+ * @private internal utility of `finalizeParsedPipeline`
10563
10724
  */
10564
- function extractAllListItemsFromMarkdown(markdown) {
10565
- const lines = markdown.split(/\r?\n/);
10566
- const listItems = [];
10567
- let isInCodeBlock = false;
10568
- for (const line of lines) {
10569
- const trimmedLine = line.trim();
10570
- if (trimmedLine.startsWith('```')) {
10571
- isInCodeBlock = !isInCodeBlock;
10572
- }
10573
- if (!isInCodeBlock && (trimmedLine.startsWith('-') || trimmedLine.match(/^\d+\./))) {
10574
- const listItem = trimmedLine.replace(/^-|\d+\./, '').trim();
10575
- listItems.push(listItem);
10725
+ function removeUndefinedProperties(entity) {
10726
+ for (const [key, value] of Object.entries(entity)) {
10727
+ if (value === undefined) {
10728
+ delete entity[key];
10576
10729
  }
10577
10730
  }
10578
- return listItems;
10579
10731
  }
10580
-
10581
10732
  /**
10582
- * Extracts exactly ONE code block from markdown.
10583
- *
10584
- * - When there are multiple or no code blocks the function throws a `ParseError`
10585
- *
10586
- * Note: There are multiple similar functions:
10587
- * - `extractBlock` just extracts the content of the code block which is also used as built-in function for postprocessing
10588
- * - `extractJsonBlock` extracts exactly one valid JSON code block
10589
- * - `extractOneBlockFromMarkdown` extracts exactly one code block with language of the code block
10590
- * - `extractAllBlocksFromMarkdown` extracts all code blocks with language of the code block
10733
+ * Applies all sync-only high-level abstractions after parsing.
10591
10734
  *
10592
- * @param markdown any valid markdown
10593
- * @returns code block with language and content
10594
- * @throws {ParseError} if there is not exactly one code block in the markdown
10735
+ * @private internal utility of `finalizeParsedPipeline`
10736
+ */
10737
+ function applySyncHighLevelAbstractions($pipelineJson) {
10738
+ for (const highLevelAbstraction of HIGH_LEVEL_ABSTRACTIONS.filter(({ type }) => type === 'SYNC')) {
10739
+ highLevelAbstraction.$applyToPipelineJson($pipelineJson);
10740
+ }
10741
+ }
10742
+ /**
10743
+ * Ensures parsed pipelines always have the default `GENERIC` formfactor.
10595
10744
  *
10596
- * @public exported from `@promptbook/markdown-utils`
10745
+ * @private internal utility of `finalizeParsedPipeline`
10597
10746
  */
10598
- function extractOneBlockFromMarkdown(markdown) {
10599
- const codeBlocks = extractAllBlocksFromMarkdown(markdown);
10600
- if (codeBlocks.length !== 1) {
10601
- throw new ParseError(_spaceTrim.spaceTrim((block) => `
10602
- There should be exactly 1 code block in task section, found ${codeBlocks.length} code blocks
10603
-
10604
- ${block(codeBlocks.map((block, i) => `Block ${i + 1}:\n${block.content}`).join('\n\n\n'))}
10605
- `));
10747
+ function ensurePipelineFormfactor($pipelineJson) {
10748
+ // Note: [🔆] If formfactor is still not set, set it to 'GENERIC'
10749
+ if ($pipelineJson.formfactorName === undefined) {
10750
+ $pipelineJson.formfactorName = 'GENERIC';
10606
10751
  }
10607
- return codeBlocks[0];
10608
10752
  }
10609
- // TODO: [🍓][🌻] Decide of this is internal utility, external util OR validator/postprocessor
10753
+ /**
10754
+ * Finalizes ordering and exports the parsed pipeline JSON.
10755
+ *
10756
+ * @private internal utility of `finalizeParsedPipeline`
10757
+ */
10758
+ function exportParsedPipelineJson($pipelineJson) {
10759
+ return exportJson({
10760
+ name: 'pipelineJson',
10761
+ message: `Result of \`parsePipeline\``,
10762
+ order: ORDER_OF_PIPELINE_JSON,
10763
+ value: {
10764
+ formfactorName: 'GENERIC',
10765
+ // <- Note: [🔆] Setting `formfactorName` is redundant to satisfy the typescript
10766
+ ...$pipelineJson,
10767
+ },
10768
+ });
10769
+ }
10770
+ // TODO: Use spaceTrim more effectively
10771
+ // TODO: [🧠] Parameter flags - isInput, isOutput, isInternal
10772
+ // TODO: [♈] Probably move expectations from tasks to parameters
10773
+ // TODO: [🍙] Make some standard order of json properties
10610
10774
 
10611
10775
  /**
10612
10776
  * Parses markdown section to title its level and content
@@ -10678,213 +10842,24 @@
10678
10842
  currentType = 'MARKDOWN';
10679
10843
  }
10680
10844
  }
10681
- else if (currentType === 'COMMENT') {
10682
- buffer.push(line);
10683
- if (line.includes('-->')) {
10684
- currentType = 'MARKDOWN';
10685
- }
10686
- }
10687
- }
10688
- finishSection();
10689
- return sections;
10690
- }
10691
- /**
10692
- * TODO: [🏛] This can be part of markdown builder
10693
- * Note: [🕞] In past (commit 42086e1603cbed506482997c00a8ee979af0a247) there was much more
10694
- * sophisticated implementation of this function through parsing markdown into JSON structure
10695
- * and flattening the actual structure
10696
- * NOW we are working just with markdown string and its good enough
10697
- */
10698
-
10699
- /**
10700
- * Normalizes the markdown by flattening the structure
10701
- *
10702
- * - It always have h1 - if there is no h1 in the markdown, it will be added `DEFAULT_BOOK_TITLE`
10703
- * - All other headings are normalized to h2
10704
- *
10705
- * @public exported from `@promptbook/markdown-utils`
10706
- */
10707
- function flattenMarkdown(markdown) {
10708
- const sections = splitMarkdownIntoSections(markdown);
10709
- if (sections.length === 0) {
10710
- return `# ${DEFAULT_BOOK_TITLE}`;
10711
- }
10712
- let flattenedMarkdown = '';
10713
- const parsedSections = sections.map(parseMarkdownSection);
10714
- const firstSection = parsedSections.shift();
10715
- if (firstSection.level === 1) {
10716
- flattenedMarkdown += `# ${firstSection.title}` + `\n\n`;
10717
- flattenedMarkdown += firstSection.content + `\n\n`; // <- [🧠] Maybe 3 new lines?
10718
- }
10719
- else {
10720
- parsedSections.unshift(firstSection);
10721
- flattenedMarkdown += `# ${DEFAULT_BOOK_TITLE}` + `\n\n`; // <- [🧠] Maybe 3 new lines?
10722
- }
10723
- for (const { title, content } of parsedSections) {
10724
- flattenedMarkdown += `## ${title}` + `\n\n`;
10725
- flattenedMarkdown += content + `\n\n`; // <- [🧠] Maybe 3 new lines?
10726
- }
10727
- return _spaceTrim.spaceTrim(flattenedMarkdown);
10728
- }
10729
- /**
10730
- * TODO: [🏛] This can be part of markdown builder
10731
- * Note: [🕞] In past (commit 42086e1603cbed506482997c00a8ee979af0a247) there was much more
10732
- * sophisticated implementation of this function through parsing markdown into JSON structure
10733
- * and flattening the actual structure
10734
- * NOW we are working just with markdown string and its good enough
10735
- */
10736
-
10737
- /**
10738
- * Normalizes inline parameter mentions wrapped in code spans before markdown flattening.
10739
- *
10740
- * @private internal utility of `parsePipeline`
10741
- */
10742
- const INLINE_CODE_PARAMETER_REGEXP = /`\{(?<parameterName>[a-z0-9_]+)\}`/gi;
10743
- /**
10744
- * Normalizes inline return statements wrapped in code spans before markdown flattening.
10745
- *
10746
- * @private internal utility of `parsePipeline`
10747
- */
10748
- const INLINE_CODE_RETURN_PARAMETER_REGEXP = /`->\s+\{(?<parameterName>[a-z0-9_]+)\}`/gi;
10749
- /**
10750
- * Matches the trailing return statement of a task section.
10751
- *
10752
- * @private internal utility of `parsePipeline`
10753
- */
10754
- const RESULTING_PARAMETER_LINE_REGEXP = /^->\s*\{(?<resultingParamName>[a-z0-9_]+)\}/im;
10755
- /**
10756
- * Removes fenced code blocks when deriving human-readable section descriptions.
10757
- *
10758
- * @private internal utility of `parsePipeline`
10759
- */
10760
- const DESCRIPTION_CODE_BLOCK_REGEXP = /^```.*^```/gms;
10761
- /**
10762
- * Removes blockquote lines when deriving human-readable section descriptions.
10763
- *
10764
- * @private internal utility of `parsePipeline`
10765
- */
10766
- const DESCRIPTION_BLOCKQUOTE_REGEXP = /^>.*$/gm;
10767
- /**
10768
- * Removes list items and return statements when deriving human-readable section descriptions.
10769
- *
10770
- * @private internal utility of `parsePipeline`
10771
- */
10772
- const DESCRIPTION_LIST_ITEM_REGEXP = /^(?:(?:-)|(?:\d\))|(?:`?->))\s+.*$/gm;
10773
- /**
10774
- * Compile pipeline from string (markdown) format to JSON format synchronously
10775
- *
10776
- * Note: There are 3 similar functions:
10777
- * - `compilePipeline` **(preferred)** - which properly compiles the promptbook and uses embedding for external knowledge
10778
- * - `parsePipeline` - use only if you need to compile promptbook synchronously and it contains NO external knowledge
10779
- * - `preparePipeline` - just one step in the compilation process
10780
- *
10781
- * Note: This function does not validate logic of the pipeline only the parsing
10782
- * Note: This function acts as compilation process
10783
- *
10784
- * @param pipelineString {Promptbook} in string markdown format (.book.md)
10785
- * @returns {Promptbook} compiled in JSON format (.bookc)
10786
- * @throws {ParseError} if the promptbook string is not valid
10787
- *
10788
- * @public exported from `@promptbook/core`
10789
- */
10790
- function parsePipeline(pipelineString) {
10791
- const $pipelineJson = createInitialPipelineJson(pipelineString);
10792
- const preparedPipelineString = preparePipelineString(pipelineString, $pipelineJson);
10793
- const { pipelineHead, pipelineSections } = parsePreparedPipelineSections(preparedPipelineString, $pipelineJson);
10794
- const getUniqueSectionName = createUniqueSectionNameResolver(pipelineSections);
10795
- applyPipelineHead(pipelineHead, $pipelineJson);
10796
- for (const pipelineSection of pipelineSections) {
10797
- processPipelineSection(pipelineSection, $pipelineJson, getUniqueSectionName);
10798
- }
10799
- applyImplicitParameterDirections($pipelineJson);
10800
- removeUndefinedValuesFromPipeline($pipelineJson);
10801
- applySyncHighLevelAbstractions($pipelineJson);
10802
- ensurePipelineFormfactor($pipelineJson);
10803
- return exportParsedPipelineJson($pipelineJson);
10804
- }
10805
- /**
10806
- * Creates the mutable pipeline JSON structure used throughout parsing.
10807
- *
10808
- * @private internal utility of `parsePipeline`
10809
- */
10810
- function createInitialPipelineJson(pipelineString) {
10811
- return {
10812
- title: DEFAULT_BOOK_TITLE,
10813
- parameters: [],
10814
- tasks: [],
10815
- knowledgeSources: [],
10816
- knowledgePieces: [],
10817
- personas: [],
10818
- preparations: [],
10819
- sources: [
10820
- {
10821
- type: 'BOOK',
10822
- path: null,
10823
- // <- TODO: !!6 Pass here path of the file
10824
- content: pipelineString,
10825
- },
10826
- ],
10827
- };
10828
- }
10829
- /**
10830
- * Builds a short file/url identification block for parse errors.
10831
- *
10832
- * @private internal utility of `parsePipeline`
10833
- */
10834
- function getPipelineIdentification($pipelineJson) {
10835
- // Note: This is a 😐 implementation of [🚞]
10836
- const pipelineIdentificationParts = [];
10837
- if ($pipelineJson.sourceFile !== undefined) {
10838
- pipelineIdentificationParts.push(`File: ${$pipelineJson.sourceFile}`);
10839
- }
10840
- if ($pipelineJson.pipelineUrl !== undefined) {
10841
- pipelineIdentificationParts.push(`Url: ${$pipelineJson.pipelineUrl}`);
10845
+ else if (currentType === 'COMMENT') {
10846
+ buffer.push(line);
10847
+ if (line.includes('-->')) {
10848
+ currentType = 'MARKDOWN';
10849
+ }
10850
+ }
10842
10851
  }
10843
- return pipelineIdentificationParts.join('\n');
10844
- }
10845
- /**
10846
- * Removes shebang/comments and normalizes markdown into a parseable pipeline form.
10847
- *
10848
- * @private internal utility of `parsePipeline`
10849
- */
10850
- function preparePipelineString(pipelineString, $pipelineJson) {
10851
- pipelineString = removePipelineShebang(pipelineString, $pipelineJson);
10852
- pipelineString = removeMarkdownComments(pipelineString);
10853
- pipelineString = _spaceTrim.spaceTrim(pipelineString);
10854
- // <- TODO: [😧] `spaceTrim` should preserve discriminated type *(or at lease `PipelineString`)*
10855
- pipelineString = deflatePipeline(pipelineString);
10856
- pipelineString = flattenMarkdown(pipelineString);
10857
- pipelineString = pipelineString.replaceAll(INLINE_CODE_PARAMETER_REGEXP, '{$<parameterName>}');
10858
- pipelineString = pipelineString.replaceAll(INLINE_CODE_RETURN_PARAMETER_REGEXP, '-> {$<parameterName>}');
10859
- return pipelineString;
10852
+ finishSection();
10853
+ return sections;
10860
10854
  }
10861
10855
  /**
10862
- * Validates and removes the optional `#!` shebang line for `.book` files.
10863
- *
10864
- * @private internal utility of `parsePipeline`
10856
+ * TODO: [🏛] This can be part of markdown builder
10857
+ * Note: [🕞] In past (commit 42086e1603cbed506482997c00a8ee979af0a247) there was much more
10858
+ * sophisticated implementation of this function through parsing markdown into JSON structure
10859
+ * and flattening the actual structure
10860
+ * NOW we are working just with markdown string and its good enough
10865
10861
  */
10866
- function removePipelineShebang(pipelineString, $pipelineJson) {
10867
- if (!pipelineString.startsWith('#!')) {
10868
- return pipelineString;
10869
- }
10870
- const [shebangLine, ...restLines] = pipelineString.split(/\r?\n/);
10871
- const isBookShebang = (shebangLine || '').includes('ptbk');
10872
- if (!isBookShebang) {
10873
- throw new ParseError(_spaceTrim.spaceTrim((block) => `
10874
- It seems that you try to parse a book file which has non-standard shebang line for book files:
10875
- Shebang line must contain 'ptbk'
10876
-
10877
- You have:
10878
- ${block(shebangLine || '(empty line)')}
10879
-
10880
- It should look like this:
10881
- #!/usr/bin/env ptbk
10882
10862
 
10883
- ${block(getPipelineIdentification($pipelineJson))}
10884
- `));
10885
- }
10886
- return validatePipelineString(restLines.join('\n'));
10887
- }
10888
10863
  /**
10889
10864
  * Splits the prepared markdown into the pipeline head and task sections.
10890
10865
  *
@@ -10901,7 +10876,7 @@
10901
10876
  /**
10902
10877
  * Ensures the flattened markdown has exactly one h1 head followed by only h2 sections.
10903
10878
  *
10904
- * @private internal utility of `parsePipeline`
10879
+ * @private internal utility of `parsePreparedPipelineSections`
10905
10880
  */
10906
10881
  function assertPipelineSectionsStructure(pipelineHead, pipelineSections, $pipelineJson) {
10907
10882
  if (pipelineHead === undefined) {
@@ -10932,148 +10907,270 @@
10932
10907
  `));
10933
10908
  }
10934
10909
  }
10910
+
10935
10911
  /**
10936
- * Applies the pipeline head title, description, and head-level commands.
10912
+ * Number of padding lines to add at the end of the book content
10937
10913
  *
10938
- * @private internal utility of `parsePipeline`
10914
+ * @public exported from `@promptbook/core`
10939
10915
  */
10940
- function applyPipelineHead(pipelineHead, $pipelineJson) {
10941
- $pipelineJson.title = pipelineHead.title;
10942
- $pipelineJson.description = extractPipelineDescription(pipelineHead.content);
10943
- for (const listItem of extractAllListItemsFromMarkdown(pipelineHead.content)) {
10944
- applyPipelineHeadCommand(listItem, $pipelineJson);
10916
+ const PADDING_LINES = 11;
10917
+ /**
10918
+ * A function that adds padding to the book content
10919
+ *
10920
+ * Note: [🔂] This function is idempotent.
10921
+ *
10922
+ * @public exported from `@promptbook/core`
10923
+ */
10924
+ function padBook(content) {
10925
+ if (!content) {
10926
+ return '\n'.repeat(PADDING_LINES);
10927
+ }
10928
+ const lines = content.split(/\r?\n/);
10929
+ let trailingEmptyLines = 0;
10930
+ for (let i = lines.length - 1; i >= 0; i--) {
10931
+ const line = lines[i];
10932
+ if (line === undefined) {
10933
+ // Note: This should not happen in reality, but it's here to satisfy TypeScript's noUncheckedIndexedAccess option
10934
+ continue;
10935
+ }
10936
+ if (line.trim() === '') {
10937
+ trailingEmptyLines++;
10938
+ }
10939
+ else {
10940
+ break;
10941
+ }
10942
+ }
10943
+ if (trailingEmptyLines >= PADDING_LINES) {
10944
+ return content;
10945
10945
  }
10946
+ const linesToAdd = PADDING_LINES - trailingEmptyLines;
10947
+ return (content + '\n'.repeat(linesToAdd));
10946
10948
  }
10949
+ // TODO: [🧠] Maybe export
10950
+
10947
10951
  /**
10948
- * Extracts the plain-text description from a head or task section body.
10952
+ * Removes Markdown (or HTML) comments
10949
10953
  *
10950
- * @private internal utility of `parsePipeline`
10954
+ * @param {string} content - The string to remove comments from.
10955
+ * @returns {string} The input string with all comments removed.
10956
+ *
10957
+ * @public exported from `@promptbook/markdown-utils`
10951
10958
  */
10952
- function extractPipelineDescription(sectionContent) {
10953
- let description = sectionContent;
10954
- description = description.split(DESCRIPTION_CODE_BLOCK_REGEXP).join('');
10955
- description = description.split(DESCRIPTION_BLOCKQUOTE_REGEXP).join('');
10956
- description = description.split(DESCRIPTION_LIST_ITEM_REGEXP).join('');
10957
- description = _spaceTrim.spaceTrim(description);
10958
- if (description === '') {
10959
- return undefined;
10960
- }
10961
- return description;
10959
+ function removeMarkdownComments(content) {
10960
+ return _spaceTrim.spaceTrim(content.replace(/<!--(.*?)-->/gs, ''));
10962
10961
  }
10962
+
10963
10963
  /**
10964
- * Parses and applies one command declared in the pipeline head.
10964
+ * Utility to determine if a pipeline string is in flat format.
10965
+ * A flat pipeline is a simple text without proper structure (headers, blocks, etc).
10965
10966
  *
10966
- * @private internal utility of `parsePipeline`
10967
+ * @public exported from `@promptbook/editable`
10967
10968
  */
10968
- function applyPipelineHeadCommand(listItem, $pipelineJson) {
10969
- const command = parseCommand(listItem, 'PIPELINE_HEAD');
10970
- const commandParser = getParserForCommand(command);
10971
- if (commandParser.isUsedInPipelineHead !== true /* <- Note: [🦦][4] */) {
10972
- throw new ParseError(_spaceTrim.spaceTrim((block) => `
10973
- Command \`${command.type}\` is not allowed in the head of the pipeline ONLY at the pipeline task
10969
+ function isFlatPipeline(pipelineString) {
10970
+ pipelineString = removeMarkdownComments(pipelineString);
10971
+ pipelineString = _spaceTrim.spaceTrim(pipelineString);
10972
+ const isMarkdownBeginningWithHeadline = pipelineString.startsWith('# ');
10973
+ //const isLastLineReturnStatement = pipelineString.split(/\r?\n/).pop()!.split('`').join('').startsWith('->');
10974
+ const isBacktickBlockUsed = pipelineString.includes('```');
10975
+ const isQuoteBlocksUsed = /^>\s+/m.test(pipelineString);
10976
+ const isBlocksUsed = isBacktickBlockUsed || isQuoteBlocksUsed;
10977
+ // TODO: [🧉] Also (double)check
10978
+ // > const usedCommands
10979
+ // > const isBlocksUsed
10980
+ // > const returnStatementCount
10981
+ const isFlat = !isMarkdownBeginningWithHeadline && !isBlocksUsed; /* && isLastLineReturnStatement */
10982
+ return isFlat;
10983
+ }
10974
10984
 
10975
- ${block(getPipelineIdentification($pipelineJson))}
10976
- `)); // <- TODO: [🚞]
10985
+ /**
10986
+ * Converts a pipeline structure to its string representation.
10987
+ *
10988
+ * Transforms a flat, simple pipeline into a properly formatted pipeline string
10989
+ * with sections for title, prompt, and return statement.
10990
+ *
10991
+ * @public exported from `@promptbook/editable`
10992
+ */
10993
+ function deflatePipeline(pipelineString) {
10994
+ if (!isFlatPipeline(pipelineString)) {
10995
+ return pipelineString;
10977
10996
  }
10978
- try {
10979
- commandParser.$applyToPipelineJson(command, $pipelineJson);
10980
- // <- Note: [🦦] Its strange that this assertion must be here, [🦦][4] should do this assertion implicitly
10997
+ pipelineString = _spaceTrim.spaceTrim(pipelineString);
10998
+ const pipelineStringLines = pipelineString.split(/\r?\n/);
10999
+ const potentialReturnStatement = pipelineStringLines.pop();
11000
+ let returnStatement;
11001
+ if (/(-|=)>\s*\{.*\}/.test(potentialReturnStatement)) {
11002
+ // Note: Last line is return statement
11003
+ returnStatement = potentialReturnStatement;
11004
+ }
11005
+ else {
11006
+ // Note: Last line is not a return statement
11007
+ returnStatement = `-> {${DEFAULT_BOOK_OUTPUT_PARAMETER_NAME}}`;
11008
+ pipelineStringLines.push(potentialReturnStatement);
11009
+ }
11010
+ const prompt = _spaceTrim.spaceTrim(pipelineStringLines.join('\n'));
11011
+ let quotedPrompt;
11012
+ if (prompt.split(/\r?\n/).length <= 1) {
11013
+ quotedPrompt = `> ${prompt}`;
11014
+ }
11015
+ else {
11016
+ quotedPrompt = _spaceTrim.spaceTrim((block) => `
11017
+ \`\`\`
11018
+ ${block(prompt.split('`').join('\\`'))}
11019
+ \`\`\`
11020
+ `);
11021
+ }
11022
+ pipelineString = validatePipelineString(_spaceTrim.spaceTrim((block) => `
11023
+ # ${DEFAULT_BOOK_TITLE}
11024
+
11025
+ ## Prompt
11026
+
11027
+ ${block(quotedPrompt)}
11028
+
11029
+ ${returnStatement}
11030
+ `));
11031
+ // <- TODO: Maybe use book` notation
11032
+ return padBook(pipelineString);
11033
+ }
11034
+ // TODO: Unit test
11035
+
11036
+ /**
11037
+ * Normalizes the markdown by flattening the structure
11038
+ *
11039
+ * - It always have h1 - if there is no h1 in the markdown, it will be added `DEFAULT_BOOK_TITLE`
11040
+ * - All other headings are normalized to h2
11041
+ *
11042
+ * @public exported from `@promptbook/markdown-utils`
11043
+ */
11044
+ function flattenMarkdown(markdown) {
11045
+ const sections = splitMarkdownIntoSections(markdown);
11046
+ if (sections.length === 0) {
11047
+ return `# ${DEFAULT_BOOK_TITLE}`;
11048
+ }
11049
+ let flattenedMarkdown = '';
11050
+ const parsedSections = sections.map(parseMarkdownSection);
11051
+ const firstSection = parsedSections.shift();
11052
+ if (firstSection.level === 1) {
11053
+ flattenedMarkdown += `# ${firstSection.title}` + `\n\n`;
11054
+ flattenedMarkdown += firstSection.content + `\n\n`; // <- [🧠] Maybe 3 new lines?
11055
+ }
11056
+ else {
11057
+ parsedSections.unshift(firstSection);
11058
+ flattenedMarkdown += `# ${DEFAULT_BOOK_TITLE}` + `\n\n`; // <- [🧠] Maybe 3 new lines?
11059
+ }
11060
+ for (const { title, content } of parsedSections) {
11061
+ flattenedMarkdown += `## ${title}` + `\n\n`;
11062
+ flattenedMarkdown += content + `\n\n`; // <- [🧠] Maybe 3 new lines?
11063
+ }
11064
+ return _spaceTrim.spaceTrim(flattenedMarkdown);
11065
+ }
11066
+ /**
11067
+ * TODO: [🏛] This can be part of markdown builder
11068
+ * Note: [🕞] In past (commit 42086e1603cbed506482997c00a8ee979af0a247) there was much more
11069
+ * sophisticated implementation of this function through parsing markdown into JSON structure
11070
+ * and flattening the actual structure
11071
+ * NOW we are working just with markdown string and its good enough
11072
+ */
11073
+
11074
+ /**
11075
+ * Normalizes inline parameter mentions wrapped in code spans before markdown flattening.
11076
+ *
11077
+ * @private internal utility of `preparePipelineString`
11078
+ */
11079
+ const INLINE_CODE_PARAMETER_REGEXP = /`\{(?<parameterName>[a-z0-9_]+)\}`/gi;
11080
+ /**
11081
+ * Normalizes inline return statements wrapped in code spans before markdown flattening.
11082
+ *
11083
+ * @private internal utility of `preparePipelineString`
11084
+ */
11085
+ const INLINE_CODE_RETURN_PARAMETER_REGEXP = /`->\s+\{(?<parameterName>[a-z0-9_]+)\}`/gi;
11086
+ /**
11087
+ * Removes shebang/comments and normalizes markdown into a parseable pipeline form.
11088
+ *
11089
+ * @private internal utility of `parsePipeline`
11090
+ */
11091
+ function preparePipelineString(pipelineString, $pipelineJson) {
11092
+ pipelineString = removePipelineShebang(pipelineString, $pipelineJson);
11093
+ pipelineString = removeMarkdownComments(pipelineString);
11094
+ pipelineString = _spaceTrim.spaceTrim(pipelineString);
11095
+ // <- TODO: [😧] `spaceTrim` should preserve discriminated type *(or at lease `PipelineString`)*
11096
+ pipelineString = deflatePipeline(pipelineString);
11097
+ pipelineString = flattenMarkdown(pipelineString);
11098
+ pipelineString = pipelineString.replaceAll(INLINE_CODE_PARAMETER_REGEXP, '{$<parameterName>}');
11099
+ pipelineString = pipelineString.replaceAll(INLINE_CODE_RETURN_PARAMETER_REGEXP, '-> {$<parameterName>}');
11100
+ return pipelineString;
11101
+ }
11102
+ /**
11103
+ * Validates and removes the optional `#!` shebang line for `.book` files.
11104
+ *
11105
+ * @private internal utility of `preparePipelineString`
11106
+ */
11107
+ function removePipelineShebang(pipelineString, $pipelineJson) {
11108
+ if (!pipelineString.startsWith('#!')) {
11109
+ return pipelineString;
10981
11110
  }
10982
- catch (error) {
10983
- if (!(error instanceof ParseError)) {
10984
- throw error;
10985
- }
11111
+ const [shebangLine, ...restLines] = pipelineString.split(/\r?\n/);
11112
+ const isBookShebang = (shebangLine || '').includes('ptbk');
11113
+ if (!isBookShebang) {
10986
11114
  throw new ParseError(_spaceTrim.spaceTrim((block) => `
10987
- Command ${command.type} failed to apply to the pipeline
10988
-
10989
- The error:
10990
- ${block(error.message)}
11115
+ It seems that you try to parse a book file which has non-standard shebang line for book files:
11116
+ Shebang line must contain 'ptbk'
10991
11117
 
10992
- Raw command:
10993
- - ${listItem}
11118
+ You have:
11119
+ ${block(shebangLine || '(empty line)')}
10994
11120
 
10995
- Usage of ${command.type}:
10996
- ${block(commandParser.examples.map((example) => `- ${example}`).join('\n'))}
11121
+ It should look like this:
11122
+ #!/usr/bin/env ptbk
10997
11123
 
10998
11124
  ${block(getPipelineIdentification($pipelineJson))}
10999
- `)); // <- TODO: [🚞]
11000
- }
11001
- if (command.type === 'PARAMETER') {
11002
- defineParameter($pipelineJson, command);
11003
- // <- Note: [🍣]
11125
+ `));
11004
11126
  }
11127
+ return validatePipelineString(restLines.join('\n'));
11005
11128
  }
11129
+
11006
11130
  /**
11007
- * Merges one parameter declaration into the mutable pipeline parameter list.
11131
+ * Supported script languages
11008
11132
  *
11009
- * @private internal utility of `parsePipeline`
11133
+ * @private internal base for `ScriptLanguage`
11010
11134
  */
11011
- function defineParameter($pipelineJson, parameterCommand) {
11012
- const { parameterName, parameterDescription, isInput, isOutput } = parameterCommand;
11013
- if (RESERVED_PARAMETER_NAMES.includes(parameterName)) {
11014
- throw new ParseError(_spaceTrim.spaceTrim((block) => `
11015
- Parameter name {${parameterName}} is reserved and cannot be used as resulting parameter name
11135
+ const SUPPORTED_SCRIPT_LANGUAGES = ['javascript', 'typescript', 'python'];
11136
+ // <- TODO: [🏥] DRY
11016
11137
 
11017
- ${block(getPipelineIdentification($pipelineJson))}
11018
- `) /* <- TODO: [🚞] */);
11019
- }
11020
- const existingParameter = $pipelineJson.parameters.find((parameter) => parameter.name === parameterName);
11021
- if (existingParameter &&
11022
- existingParameter.description &&
11023
- existingParameter.description !== parameterDescription &&
11024
- parameterDescription) {
11138
+ /**
11139
+ * Extracts exactly ONE code block from markdown.
11140
+ *
11141
+ * - When there are multiple or no code blocks the function throws a `ParseError`
11142
+ *
11143
+ * Note: There are multiple similar functions:
11144
+ * - `extractBlock` just extracts the content of the code block which is also used as built-in function for postprocessing
11145
+ * - `extractJsonBlock` extracts exactly one valid JSON code block
11146
+ * - `extractOneBlockFromMarkdown` extracts exactly one code block with language of the code block
11147
+ * - `extractAllBlocksFromMarkdown` extracts all code blocks with language of the code block
11148
+ *
11149
+ * @param markdown any valid markdown
11150
+ * @returns code block with language and content
11151
+ * @throws {ParseError} if there is not exactly one code block in the markdown
11152
+ *
11153
+ * @public exported from `@promptbook/markdown-utils`
11154
+ */
11155
+ function extractOneBlockFromMarkdown(markdown) {
11156
+ const codeBlocks = extractAllBlocksFromMarkdown(markdown);
11157
+ if (codeBlocks.length !== 1) {
11025
11158
  throw new ParseError(_spaceTrim.spaceTrim((block) => `
11026
- Parameter \`{${parameterName}}\` is defined multiple times with different description:
11027
-
11028
- ${block(getPipelineIdentification($pipelineJson))}
11029
-
11030
- First definition:
11031
- ${block(existingParameter.description || '[undefined]')}
11159
+ There should be exactly 1 code block in task section, found ${codeBlocks.length} code blocks
11032
11160
 
11033
- Second definition:
11034
- ${block(parameterDescription || '[undefined]')}
11161
+ ${block(codeBlocks.map((block, i) => `Block ${i + 1}:\n${block.content}`).join('\n\n\n'))}
11035
11162
  `));
11036
11163
  }
11037
- if (existingParameter) {
11038
- if (parameterDescription) {
11039
- existingParameter.description = parameterDescription;
11040
- }
11041
- existingParameter.isInput = existingParameter.isInput || isInput;
11042
- existingParameter.isOutput = existingParameter.isOutput || isOutput;
11043
- return;
11044
- }
11045
- $pipelineJson.parameters.push({
11046
- name: parameterName,
11047
- description: parameterDescription || undefined,
11048
- isInput,
11049
- isOutput,
11050
- });
11164
+ return codeBlocks[0];
11051
11165
  }
11166
+ // TODO: [🍓][🌻] Decide of this is internal utility, external util OR validator/postprocessor
11167
+
11052
11168
  /**
11053
- * Creates stable unique task names for duplicate section titles.
11169
+ * Matches the trailing return statement of a task section.
11054
11170
  *
11055
- * @private internal utility of `parsePipeline`
11171
+ * @private internal utility of `processPipelineSection`
11056
11172
  */
11057
- function createUniqueSectionNameResolver(pipelineSections) {
11058
- const sectionCounts = {};
11059
- for (const pipelineSection of pipelineSections) {
11060
- const sectionName = titleToName(pipelineSection.title);
11061
- if (sectionCounts[sectionName] === undefined) {
11062
- sectionCounts[sectionName] = { count: 0, currentIndex: 0 };
11063
- }
11064
- sectionCounts[sectionName].count++;
11065
- }
11066
- return (title) => {
11067
- const sectionName = titleToName(title);
11068
- const sectionCount = sectionCounts[sectionName];
11069
- if (sectionCount.count === 1) {
11070
- return sectionName;
11071
- }
11072
- const nameWithSuffix = `${sectionName}-${sectionCount.currentIndex}`;
11073
- sectionCount.currentIndex++;
11074
- return nameWithSuffix;
11075
- };
11076
- }
11173
+ const RESULTING_PARAMETER_LINE_REGEXP = /^->\s*\{(?<resultingParamName>[a-z0-9_]+)\}/im;
11077
11174
  /**
11078
11175
  * Parses, applies, and persists one h2 task section.
11079
11176
  *
@@ -11093,7 +11190,7 @@
11093
11190
  /**
11094
11191
  * Creates the mutable task JSON shell from one markdown section.
11095
11192
  *
11096
- * @private internal utility of `parsePipeline`
11193
+ * @private internal utility of `processPipelineSection`
11097
11194
  */
11098
11195
  function createTaskJsonFromSection(pipelineSection, getUniqueSectionName) {
11099
11196
  const { language, content } = extractOneBlockFromMarkdown(pipelineSection.content);
@@ -11121,7 +11218,7 @@
11121
11218
  /**
11122
11219
  * Extracts the optional trailing `-> {parameter}` statement from a section body.
11123
11220
  *
11124
- * @private internal utility of `parsePipeline`
11221
+ * @private internal utility of `processPipelineSection`
11125
11222
  */
11126
11223
  function extractResultingParameterName(sectionContent) {
11127
11224
  var _a;
@@ -11132,7 +11229,7 @@
11132
11229
  /**
11133
11230
  * Parses all list-item commands declared inside one task section.
11134
11231
  *
11135
- * @private internal utility of `parsePipeline`
11232
+ * @private internal utility of `processPipelineSection`
11136
11233
  */
11137
11234
  function parsePipelineTaskCommands(sectionContent) {
11138
11235
  return extractAllListItemsFromMarkdown(sectionContent).map((listItem) => ({
@@ -11143,7 +11240,7 @@
11143
11240
  /**
11144
11241
  * Applies the implicit default `PROMPT_TASK` section type when no SECTION command is present.
11145
11242
  *
11146
- * @private internal utility of `parsePipeline`
11243
+ * @private internal utility of `processPipelineSection`
11147
11244
  */
11148
11245
  function applyDefaultTaskSectionType($taskJson, commands, $pipelineJson) {
11149
11246
  const isSectionCommandPresent = commands.some(({ command }) => command.type === 'SECTION');
@@ -11155,7 +11252,7 @@
11155
11252
  /**
11156
11253
  * Parses and applies one command declared inside a task section.
11157
11254
  *
11158
- * @private internal utility of `parsePipeline`
11255
+ * @private internal utility of `processPipelineSection`
11159
11256
  */
11160
11257
  function applyPipelineTaskCommand(commandItem, $taskJson, $pipelineJson) {
11161
11258
  const { listItem, command } = commandItem;
@@ -11206,7 +11303,7 @@
11206
11303
  /**
11207
11304
  * Validates and stores the language for SCRIPT tasks.
11208
11305
  *
11209
- * @private internal utility of `parsePipeline`
11306
+ * @private internal utility of `processPipelineSection`
11210
11307
  */
11211
11308
  function applyScriptTaskLanguage($taskJson, language, $pipelineJson) {
11212
11309
  const isScriptTask = $taskJson.taskType === 'SCRIPT_TASK';
@@ -11223,7 +11320,6 @@
11223
11320
  if (!SUPPORTED_SCRIPT_LANGUAGES.includes(language)) {
11224
11321
  throw new ParseError(_spaceTrim.spaceTrim((block) => `
11225
11322
  Script language ${language} is not supported.
11226
-
11227
11323
  Supported languages are:
11228
11324
  ${block(SUPPORTED_SCRIPT_LANGUAGES.join(', '))}
11229
11325
 
@@ -11232,168 +11328,87 @@
11232
11328
  $taskJson.contentLanguage = language;
11233
11329
  }
11234
11330
  /**
11235
- * Extracts task dependencies and ensures referenced parameters exist.
11236
- *
11237
- * @private internal utility of `parsePipeline`
11238
- */
11239
- function registerTaskDependentParameters($taskJson, $pipelineJson) {
11240
- $taskJson.dependentParameterNames = Array.from(extractParameterNamesFromTask($taskJson));
11241
- for (const parameterName of $taskJson.dependentParameterNames) {
11242
- // TODO: [🧠] This definition should be made first in the task
11243
- defineParameter($pipelineJson, {
11244
- parameterName,
11245
- parameterDescription: null,
11246
- isInput: false,
11247
- isOutput: false,
11248
- // <- Note: In this case null+false+false means that we do not know yet if it is input or output and we will set it later
11249
- });
11250
- }
11251
- }
11252
- /**
11253
- * Removes transient parsing flags and persists real tasks into the pipeline JSON.
11254
- *
11255
- * @private internal utility of `parsePipeline`
11256
- */
11257
- function persistTaskIfNeeded($taskJson, $pipelineJson) {
11258
- /*
11259
- // TODO: [🍧] This should be checked in `MODEL` command + better error message
11260
- if ($taskJson.taskType !== 'PROMPT_TASK' && $taskJson.modelRequirements !== undefined) {
11261
- throw new UnexpectedError(
11262
- spaceTrim(
11263
- (block) => `
11264
- Model requirements are defined for the block type ${
11265
- $taskJson.taskType
11266
- } which is not a \`PROMPT\` task
11267
-
11268
- This should be avoided by the \`modelCommandParser\`
11269
-
11270
- ${block(getPipelineIdentification($pipelineJson))}
11271
- `,
11272
- ),
11273
- );
11274
- }
11275
- */
11276
- if (!$taskJson.isTask) {
11277
- return;
11278
- }
11279
- delete $taskJson.isSectionTypeSet;
11280
- delete $taskJson.isTask;
11281
- // TODO: [🍙] Maybe do reorder of `$taskJson` here
11282
- $pipelineJson.tasks.push($taskJson);
11283
- }
11284
- /**
11285
- * Applies default INPUT/OUTPUT flags when the author did not specify them explicitly.
11286
- *
11287
- * @private internal utility of `parsePipeline`
11288
- */
11289
- function applyImplicitParameterDirections($pipelineJson) {
11290
- markImplicitInputParameters($pipelineJson);
11291
- markImplicitOutputParameters($pipelineJson);
11292
- }
11293
- /**
11294
- * Marks non-result parameters as pipeline inputs when no input was declared.
11295
- *
11296
- * @private internal utility of `parsePipeline`
11297
- */
11298
- function markImplicitInputParameters($pipelineJson) {
11299
- if ($pipelineJson.parameters.some((parameter) => parameter.isInput)) {
11300
- return;
11301
- }
11302
- for (const parameter of $pipelineJson.parameters) {
11303
- const isThisParameterResulting = $pipelineJson.tasks.some((task) => task.resultingParameterName === parameter.name);
11304
- if (!isThisParameterResulting) {
11305
- parameter.isInput = true;
11306
- // <- TODO: [💔] Why this is making typescript error in vscode but not in cli
11307
- // > Type 'true' is not assignable to type 'false'.ts(2322)
11308
- // > (property) isInput: false
11309
- // > The parameter is input of the pipeline The parameter is NOT input of the pipeline
11310
- }
11311
- }
11312
- }
11313
- /**
11314
- * Marks every non-input parameter as output when no output was declared.
11315
- *
11316
- * @private internal utility of `parsePipeline`
11317
- */
11318
- function markImplicitOutputParameters($pipelineJson) {
11319
- if ($pipelineJson.parameters.some((parameter) => parameter.isOutput)) {
11320
- return;
11321
- }
11322
- for (const parameter of $pipelineJson.parameters) {
11323
- if (!parameter.isInput) {
11324
- parameter.isOutput = true;
11325
- // <- TODO: [💔]
11326
- }
11327
- }
11328
- }
11329
- /**
11330
- * Removes `undefined` properties from serialized tasks and parameters.
11331
- *
11332
- * @private internal utility of `parsePipeline`
11333
- */
11334
- function removeUndefinedValuesFromPipeline($pipelineJson) {
11335
- $pipelineJson.tasks.forEach(removeUndefinedProperties);
11336
- $pipelineJson.parameters.forEach(removeUndefinedProperties);
11337
- }
11338
- /**
11339
- * Deletes all own properties with `undefined` values from a mutable JSON entity.
11340
- *
11341
- * @private internal utility of `parsePipeline`
11342
- */
11343
- function removeUndefinedProperties(entity) {
11344
- for (const [key, value] of Object.entries(entity)) {
11345
- if (value === undefined) {
11346
- delete entity[key];
11347
- }
11348
- }
11349
- }
11350
- /**
11351
- * Applies all sync-only high-level abstractions after parsing.
11331
+ * Extracts task dependencies and ensures referenced parameters exist.
11352
11332
  *
11353
- * @private internal utility of `parsePipeline`
11333
+ * @private internal utility of `processPipelineSection`
11354
11334
  */
11355
- function applySyncHighLevelAbstractions($pipelineJson) {
11356
- for (const highLevelAbstraction of HIGH_LEVEL_ABSTRACTIONS.filter(({ type }) => type === 'SYNC')) {
11357
- highLevelAbstraction.$applyToPipelineJson($pipelineJson);
11335
+ function registerTaskDependentParameters($taskJson, $pipelineJson) {
11336
+ $taskJson.dependentParameterNames = Array.from(extractParameterNamesFromTask($taskJson));
11337
+ for (const parameterName of $taskJson.dependentParameterNames) {
11338
+ // TODO: [🧠] This definition should be made first in the task
11339
+ defineParameter($pipelineJson, {
11340
+ parameterName,
11341
+ parameterDescription: null,
11342
+ isInput: false,
11343
+ isOutput: false,
11344
+ // <- Note: In this case null+false+false means that we do not know yet if it is input or output and we will set it later
11345
+ });
11358
11346
  }
11359
11347
  }
11360
11348
  /**
11361
- * Ensures parsed pipelines always have the default `GENERIC` formfactor.
11349
+ * Removes transient parsing flags and persists real tasks into the pipeline JSON.
11362
11350
  *
11363
- * @private internal utility of `parsePipeline`
11351
+ * @private internal utility of `processPipelineSection`
11364
11352
  */
11365
- function ensurePipelineFormfactor($pipelineJson) {
11366
- // Note: [🔆] If formfactor is still not set, set it to 'GENERIC'
11367
- if ($pipelineJson.formfactorName === undefined) {
11368
- $pipelineJson.formfactorName = 'GENERIC';
11353
+ function persistTaskIfNeeded($taskJson, $pipelineJson) {
11354
+ /*
11355
+ // TODO: [🍧] This should be checked in `MODEL` command + better error message
11356
+ if ($taskJson.taskType !== 'PROMPT_TASK' && $taskJson.modelRequirements !== undefined) {
11357
+ throw new UnexpectedError(
11358
+ spaceTrim(
11359
+ (block) => `
11360
+ Model requirements are defined for the block type ${
11361
+ $taskJson.taskType
11362
+ } which is not a \`PROMPT\` task
11363
+
11364
+ This should be avoided by the \`modelCommandParser\`
11365
+
11366
+ ${block(getPipelineIdentification($pipelineJson))}
11367
+ `,
11368
+ ),
11369
+ );
11370
+ }
11371
+ */
11372
+ if (!$taskJson.isTask) {
11373
+ return;
11369
11374
  }
11375
+ delete $taskJson.isSectionTypeSet;
11376
+ delete $taskJson.isTask;
11377
+ // TODO: [🍙] Maybe do reorder of `$taskJson` here
11378
+ $pipelineJson.tasks.push($taskJson);
11370
11379
  }
11380
+
11371
11381
  /**
11372
- * Finalizes ordering and exports the parsed pipeline JSON.
11382
+ * Compile pipeline from string (markdown) format to JSON format synchronously
11373
11383
  *
11374
- * @private internal utility of `parsePipeline`
11384
+ * Note: There are 3 similar functions:
11385
+ * - `compilePipeline` **(preferred)** - which properly compiles the promptbook and uses embedding for external knowledge
11386
+ * - `parsePipeline` - use only if you need to compile promptbook synchronously and it contains NO external knowledge
11387
+ * - `preparePipeline` - just one step in the compilation process
11388
+ *
11389
+ * Note: This function does not validate logic of the pipeline only the parsing
11390
+ * Note: This function acts as compilation process
11391
+ *
11392
+ * @param pipelineString {Promptbook} in string markdown format (.book.md)
11393
+ * @returns {Promptbook} compiled in JSON format (.bookc)
11394
+ * @throws {ParseError} if the promptbook string is not valid
11395
+ *
11396
+ * @public exported from `@promptbook/core`
11375
11397
  */
11376
- function exportParsedPipelineJson($pipelineJson) {
11377
- return exportJson({
11378
- name: 'pipelineJson',
11379
- message: `Result of \`parsePipeline\``,
11380
- order: ORDER_OF_PIPELINE_JSON,
11381
- value: {
11382
- formfactorName: 'GENERIC',
11383
- // <- Note: [🔆] Setting `formfactorName` is redundant to satisfy the typescript
11384
- ...$pipelineJson,
11385
- },
11386
- });
11398
+ function parsePipeline(pipelineString) {
11399
+ const $pipelineJson = createInitialPipelineJson(pipelineString);
11400
+ const preparedPipelineString = preparePipelineString(pipelineString, $pipelineJson);
11401
+ const { pipelineHead, pipelineSections } = parsePreparedPipelineSections(preparedPipelineString, $pipelineJson);
11402
+ const getUniqueSectionName = createUniqueSectionNameResolver(pipelineSections);
11403
+ applyPipelineHead(pipelineHead, $pipelineJson);
11404
+ for (const pipelineSection of pipelineSections) {
11405
+ processPipelineSection(pipelineSection, $pipelineJson, getUniqueSectionName);
11406
+ }
11407
+ return finalizeParsedPipeline($pipelineJson);
11387
11408
  }
11388
11409
  // TODO: [🧠] Maybe more things here can be refactored as high-level abstractions
11389
11410
  // TODO: [main] !!4 Warn if used only sync version
11390
11411
  // TODO: [🚞] Report here line/column of error
11391
- // TODO: Use spaceTrim more effectively
11392
- // TODO: [🧠] Parameter flags - isInput, isOutput, isInternal
11393
- // TODO: [🥞] Not optimal parsing because `splitMarkdownIntoSections` is executed twice with same string, once through `flattenMarkdown` and second directly here
11394
- // TODO: [♈] Probably move expectations from tasks to parameters
11395
- // TODO: [🛠] Actions, instruments (and maybe knowledge) => Functions and tools
11396
- // TODO: [🍙] Make some standard order of json properties
11397
11412
 
11398
11413
  /**
11399
11414
  * Compile pipeline from string (markdown) format to JSON format
@@ -18141,7 +18156,9 @@
18141
18156
  };
18142
18157
  const mantleRadiusX = size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch;
18143
18158
  const mantleRadiusY = size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.1;
18144
- const mantleRadiusZ = size * morphologyProfile.body.bodyRadiusRatio * (0.9 + (morphologyProfile.body.horizontalStretch - 1) * 0.3);
18159
+ const mantleRadiusZ = size *
18160
+ morphologyProfile.body.bodyRadiusRatio *
18161
+ (0.9 + (morphologyProfile.body.horizontalStretch - 1) * 0.3);
18145
18162
  const underbodyRadiusX = mantleRadiusX * (0.9 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.08);
18146
18163
  const underbodyRadiusY = mantleRadiusY * (0.44 + morphologyProfile.body.lowerDropRatio * 3.1);
18147
18164
  const underbodyRadiusZ = mantleRadiusZ * 0.78;
@@ -18227,7 +18244,11 @@
18227
18244
  z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, faceEyeSpacing, faceEyeYOffset),
18228
18245
  }, faceEyeRadiusX, faceEyeRadiusY, mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.7 + eyeRandom() * 0.6, interaction, morphologyProfile.face.eyeStyle);
18229
18246
  drawProjectedMouth(context, [
18230
- { x: -mouthHalfWidth, y: mouthY, z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, -mouthHalfWidth, mouthY) },
18247
+ {
18248
+ x: -mouthHalfWidth,
18249
+ y: mouthY,
18250
+ z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, -mouthHalfWidth, mouthY),
18251
+ },
18231
18252
  {
18232
18253
  x: size * morphologyProfile.face.mouthCenterOffsetRatio,
18233
18254
  y: mouthY +
@@ -18236,7 +18257,11 @@
18236
18257
  interaction.gazeY * size * 0.01,
18237
18258
  z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, size * morphologyProfile.face.mouthCenterOffsetRatio, mouthY),
18238
18259
  },
18239
- { x: mouthHalfWidth, y: mouthY, z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, mouthHalfWidth, mouthY) },
18260
+ {
18261
+ x: mouthHalfWidth,
18262
+ y: mouthY,
18263
+ z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, mouthHalfWidth, mouthY),
18264
+ },
18240
18265
  ], mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, palette, size);
18241
18266
  },
18242
18267
  };
@@ -18397,7 +18422,8 @@
18397
18422
  z: anchorPoint.z + Math.cos(orbitAngle) * depthReach * 0.3 + sway * size * 0.012,
18398
18423
  };
18399
18424
  const controlPointTwo = {
18400
- x: anchorPoint.x + Math.sin(orbitAngle) * lateralReach * (0.82 + morphologyProfile.tentacles.swayScale * 0.12),
18425
+ x: anchorPoint.x +
18426
+ Math.sin(orbitAngle) * lateralReach * (0.82 + morphologyProfile.tentacles.swayScale * 0.12),
18401
18427
  y: anchorPoint.y + flowLength * 0.66,
18402
18428
  z: anchorPoint.z + Math.cos(orbitAngle) * depthReach * 0.72 + sway * size * 0.02,
18403
18429
  };
@@ -18462,8 +18488,7 @@
18462
18488
  context.beginPath();
18463
18489
  context.moveTo(startPoint.x, startPoint.y);
18464
18490
  context.lineTo(endPoint.x, endPoint.y);
18465
- context.strokeStyle =
18466
- tentacleStroke.colorBias > 0.6 ? `${palette.secondary}f0` : `${palette.primary}f0`;
18491
+ context.strokeStyle = tentacleStroke.colorBias > 0.6 ? `${palette.secondary}f0` : `${palette.primary}f0`;
18467
18492
  context.lineWidth = width;
18468
18493
  context.lineCap = 'round';
18469
18494
  context.stroke();
@@ -26658,6 +26683,212 @@
26658
26683
  * @private internal USE TIMEOUT constant
26659
26684
  */
26660
26685
  const MAX_LIST_TIMEOUTS_LIMIT = 100;
26686
+ /**
26687
+ * Creates one formatted timeout-argument validation error.
26688
+ *
26689
+ * @private internal utility of USE TIMEOUT
26690
+ */
26691
+ function createTimeoutToolArgsError(message) {
26692
+ return new PipelineExecutionError(_spaceTrim.spaceTrim(`
26693
+ ${message}
26694
+ `));
26695
+ }
26696
+ /**
26697
+ * Normalizes one optional timeout id string.
26698
+ *
26699
+ * @private internal utility of USE TIMEOUT
26700
+ */
26701
+ function normalizeOptionalTimeoutId(value) {
26702
+ return typeof value === 'string' ? value.trim() : '';
26703
+ }
26704
+ /**
26705
+ * Parses timeout target selection for tools that accept either `timeoutId` or `allActive: true`.
26706
+ *
26707
+ * @private internal utility of USE TIMEOUT
26708
+ */
26709
+ function parseTimeoutTargetSelection(args, options) {
26710
+ const timeoutId = normalizeOptionalTimeoutId(args.timeoutId);
26711
+ const allActive = args.allActive === true;
26712
+ if (timeoutId && allActive) {
26713
+ throw createTimeoutToolArgsError(options.bothMessage);
26714
+ }
26715
+ if (allActive) {
26716
+ return { allActive: true };
26717
+ }
26718
+ if (!timeoutId) {
26719
+ throw createTimeoutToolArgsError(options.missingMessage);
26720
+ }
26721
+ return {
26722
+ timeoutId,
26723
+ allActive: false,
26724
+ };
26725
+ }
26726
+ /**
26727
+ * Parses one explicit `dueAt` update value.
26728
+ *
26729
+ * @private internal utility of USE TIMEOUT
26730
+ */
26731
+ function parseOptionalTimeoutDueAt(value) {
26732
+ if (typeof value !== 'string' || value.trim().length === 0) {
26733
+ return undefined;
26734
+ }
26735
+ const normalizedDueAt = value.trim();
26736
+ const dueAtTimestamp = Date.parse(normalizedDueAt);
26737
+ if (!Number.isFinite(dueAtTimestamp)) {
26738
+ throw createTimeoutToolArgsError('Timeout `dueAt` must be one valid ISO timestamp.');
26739
+ }
26740
+ return new Date(dueAtTimestamp).toISOString();
26741
+ }
26742
+ /**
26743
+ * Parses one explicit `extendByMs` update value.
26744
+ *
26745
+ * @private internal utility of USE TIMEOUT
26746
+ */
26747
+ function parseOptionalTimeoutExtendByMs(value) {
26748
+ if (typeof value !== 'number') {
26749
+ return undefined;
26750
+ }
26751
+ if (!Number.isFinite(value) || value <= 0) {
26752
+ throw createTimeoutToolArgsError('Timeout `extendByMs` must be a positive number of milliseconds.');
26753
+ }
26754
+ return Math.floor(value);
26755
+ }
26756
+ /**
26757
+ * Parses one explicit `recurrenceIntervalMs` update value.
26758
+ *
26759
+ * @private internal utility of USE TIMEOUT
26760
+ */
26761
+ function parseOptionalTimeoutRecurrenceInterval(value) {
26762
+ if (value === null) {
26763
+ return null;
26764
+ }
26765
+ if (typeof value !== 'number') {
26766
+ return undefined;
26767
+ }
26768
+ if (!Number.isFinite(value) || value <= 0) {
26769
+ throw createTimeoutToolArgsError('Timeout `recurrenceIntervalMs` must be a positive number of milliseconds or `null`.');
26770
+ }
26771
+ return Math.floor(value);
26772
+ }
26773
+ /**
26774
+ * Parses one explicit `message` update value.
26775
+ *
26776
+ * @private internal utility of USE TIMEOUT
26777
+ */
26778
+ function parseOptionalTimeoutMessage(value) {
26779
+ if (value === null) {
26780
+ return null;
26781
+ }
26782
+ if (typeof value !== 'string') {
26783
+ return undefined;
26784
+ }
26785
+ const normalizedMessage = value.trim();
26786
+ return normalizedMessage.length > 0 ? normalizedMessage : null;
26787
+ }
26788
+ /**
26789
+ * Parses one explicit `parameters` update value.
26790
+ *
26791
+ * @private internal utility of USE TIMEOUT
26792
+ */
26793
+ function parseOptionalTimeoutParameters(value) {
26794
+ if (value === undefined) {
26795
+ return undefined;
26796
+ }
26797
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
26798
+ throw createTimeoutToolArgsError('Timeout `parameters` must be one JSON object.');
26799
+ }
26800
+ return value;
26801
+ }
26802
+ /**
26803
+ * Parses one explicit `paused` update value.
26804
+ *
26805
+ * @private internal utility of USE TIMEOUT
26806
+ */
26807
+ function parseOptionalTimeoutPaused(value) {
26808
+ return typeof value === 'boolean' ? value : undefined;
26809
+ }
26810
+ /**
26811
+ * Parses patch fields for `update_timeout`.
26812
+ *
26813
+ * @private internal utility of USE TIMEOUT
26814
+ */
26815
+ function parseTimeoutUpdatePatch(args) {
26816
+ const patch = {};
26817
+ const dueAt = parseOptionalTimeoutDueAt(args.dueAt);
26818
+ const extendByMs = parseOptionalTimeoutExtendByMs(args.extendByMs);
26819
+ const recurrenceIntervalMs = parseOptionalTimeoutRecurrenceInterval(args.recurrenceIntervalMs);
26820
+ const message = parseOptionalTimeoutMessage(args.message);
26821
+ const parameters = parseOptionalTimeoutParameters(args.parameters);
26822
+ const paused = parseOptionalTimeoutPaused(args.paused);
26823
+ if (dueAt !== undefined) {
26824
+ patch.dueAt = dueAt;
26825
+ }
26826
+ if (extendByMs !== undefined) {
26827
+ patch.extendByMs = extendByMs;
26828
+ }
26829
+ if (patch.dueAt !== undefined && patch.extendByMs !== undefined) {
26830
+ throw createTimeoutToolArgsError('Timeout update cannot include both `dueAt` and `extendByMs`.');
26831
+ }
26832
+ if (recurrenceIntervalMs !== undefined) {
26833
+ patch.recurrenceIntervalMs = recurrenceIntervalMs;
26834
+ }
26835
+ if (message !== undefined) {
26836
+ patch.message = message;
26837
+ }
26838
+ if (parameters !== undefined) {
26839
+ patch.parameters = parameters;
26840
+ }
26841
+ if (paused !== undefined) {
26842
+ patch.paused = paused;
26843
+ }
26844
+ return patch;
26845
+ }
26846
+ /**
26847
+ * Determines whether the patch contains fields that are only supported for single-timeout updates.
26848
+ *
26849
+ * @private internal utility of USE TIMEOUT
26850
+ */
26851
+ function hasSingleTimeoutOnlyPatchFields(patch) {
26852
+ return (patch.dueAt !== undefined ||
26853
+ patch.extendByMs !== undefined ||
26854
+ patch.recurrenceIntervalMs !== undefined ||
26855
+ patch.message !== undefined ||
26856
+ patch.parameters !== undefined);
26857
+ }
26858
+ /**
26859
+ * Parses bulk timeout update arguments.
26860
+ *
26861
+ * @private internal utility of USE TIMEOUT
26862
+ */
26863
+ function parseBulkTimeoutUpdateArgs(patch) {
26864
+ if (patch.paused === undefined) {
26865
+ throw createTimeoutToolArgsError('Bulk timeout update with `allActive: true` requires `paused` to be explicitly set.');
26866
+ }
26867
+ if (hasSingleTimeoutOnlyPatchFields(patch)) {
26868
+ throw createTimeoutToolArgsError('Bulk timeout update only supports the `paused` field.');
26869
+ }
26870
+ return {
26871
+ allActive: true,
26872
+ paused: patch.paused,
26873
+ };
26874
+ }
26875
+ /**
26876
+ * Parses single-timeout update arguments.
26877
+ *
26878
+ * @private internal utility of USE TIMEOUT
26879
+ */
26880
+ function parseSingleTimeoutUpdateArgs(timeoutId, patch) {
26881
+ if (!timeoutId) {
26882
+ throw createTimeoutToolArgsError('Timeout `timeoutId` is required for single-timeout updates.');
26883
+ }
26884
+ if (Object.keys(patch).length === 0) {
26885
+ throw createTimeoutToolArgsError('Timeout update must include at least one editable field.');
26886
+ }
26887
+ return {
26888
+ timeoutId,
26889
+ patch,
26890
+ };
26891
+ }
26661
26892
  /**
26662
26893
  * Parses and validates `USE TIMEOUT` tool arguments.
26663
26894
  *
@@ -26684,22 +26915,14 @@
26684
26915
  * Parses `cancel_timeout` input.
26685
26916
  */
26686
26917
  cancel(args) {
26687
- const timeoutId = typeof args.timeoutId === 'string' ? args.timeoutId.trim() : '';
26688
- const allActive = args.allActive === true;
26689
- if (timeoutId && allActive) {
26690
- throw new PipelineExecutionError(_spaceTrim.spaceTrim(`
26691
- Timeout cancellation must target either one \`timeoutId\` or \`allActive: true\`, not both.
26692
- `));
26693
- }
26694
- if (allActive) {
26918
+ const target = parseTimeoutTargetSelection(args, {
26919
+ bothMessage: 'Timeout cancellation must target either one `timeoutId` or `allActive: true`, not both.',
26920
+ missingMessage: 'Timeout `timeoutId` is required unless you pass `allActive: true`.',
26921
+ });
26922
+ if (target.allActive) {
26695
26923
  return { allActive: true };
26696
26924
  }
26697
- if (!timeoutId) {
26698
- throw new PipelineExecutionError(_spaceTrim.spaceTrim(`
26699
- Timeout \`timeoutId\` is required unless you pass \`allActive: true\`.
26700
- `));
26701
- }
26702
- return { timeoutId };
26925
+ return { timeoutId: target.timeoutId };
26703
26926
  },
26704
26927
  /**
26705
26928
  * Parses `list_timeouts` input.
@@ -26730,106 +26953,14 @@
26730
26953
  * Parses `update_timeout` input.
26731
26954
  */
26732
26955
  update(args) {
26733
- const timeoutId = typeof args.timeoutId === 'string' ? args.timeoutId.trim() : '';
26734
- const allActive = args.allActive === true;
26735
- if (timeoutId && allActive) {
26736
- throw new PipelineExecutionError(_spaceTrim.spaceTrim(`
26737
- Timeout update must target either one \`timeoutId\` or \`allActive: true\`, not both.
26738
- `));
26739
- }
26740
- if (!timeoutId && !allActive) {
26741
- throw new PipelineExecutionError(_spaceTrim.spaceTrim(`
26742
- Timeout update requires one \`timeoutId\` or \`allActive: true\`.
26743
- `));
26744
- }
26745
- const patch = {};
26746
- if (typeof args.dueAt === 'string' && args.dueAt.trim().length > 0) {
26747
- const normalizedDueAt = args.dueAt.trim();
26748
- const dueAtTimestamp = Date.parse(normalizedDueAt);
26749
- if (!Number.isFinite(dueAtTimestamp)) {
26750
- throw new PipelineExecutionError(_spaceTrim.spaceTrim(`
26751
- Timeout \`dueAt\` must be one valid ISO timestamp.
26752
- `));
26753
- }
26754
- patch.dueAt = new Date(dueAtTimestamp).toISOString();
26755
- }
26756
- if (typeof args.extendByMs === 'number') {
26757
- if (!Number.isFinite(args.extendByMs) || args.extendByMs <= 0) {
26758
- throw new PipelineExecutionError(_spaceTrim.spaceTrim(`
26759
- Timeout \`extendByMs\` must be a positive number of milliseconds.
26760
- `));
26761
- }
26762
- patch.extendByMs = Math.floor(args.extendByMs);
26763
- }
26764
- if (patch.dueAt !== undefined && patch.extendByMs !== undefined) {
26765
- throw new PipelineExecutionError(_spaceTrim.spaceTrim(`
26766
- Timeout update cannot include both \`dueAt\` and \`extendByMs\`.
26767
- `));
26768
- }
26769
- if (args.recurrenceIntervalMs === null) {
26770
- patch.recurrenceIntervalMs = null;
26771
- }
26772
- else if (typeof args.recurrenceIntervalMs === 'number') {
26773
- if (!Number.isFinite(args.recurrenceIntervalMs) || args.recurrenceIntervalMs <= 0) {
26774
- throw new PipelineExecutionError(_spaceTrim.spaceTrim(`
26775
- Timeout \`recurrenceIntervalMs\` must be a positive number of milliseconds or \`null\`.
26776
- `));
26777
- }
26778
- patch.recurrenceIntervalMs = Math.floor(args.recurrenceIntervalMs);
26779
- }
26780
- if (args.message === null) {
26781
- patch.message = null;
26782
- }
26783
- else if (typeof args.message === 'string') {
26784
- const normalizedMessage = args.message.trim();
26785
- patch.message = normalizedMessage.length > 0 ? normalizedMessage : null;
26786
- }
26787
- if (args.parameters !== undefined) {
26788
- if (!args.parameters || typeof args.parameters !== 'object' || Array.isArray(args.parameters)) {
26789
- throw new PipelineExecutionError(_spaceTrim.spaceTrim(`
26790
- Timeout \`parameters\` must be one JSON object.
26791
- `));
26792
- }
26793
- patch.parameters = args.parameters;
26794
- }
26795
- if (typeof args.paused === 'boolean') {
26796
- patch.paused = args.paused;
26797
- }
26798
- if (allActive) {
26799
- if (patch.paused === undefined) {
26800
- throw new PipelineExecutionError(_spaceTrim.spaceTrim(`
26801
- Bulk timeout update with \`allActive: true\` requires \`paused\` to be explicitly set.
26802
- `));
26803
- }
26804
- const hasSingleOnlyPatch = patch.dueAt !== undefined ||
26805
- patch.extendByMs !== undefined ||
26806
- patch.recurrenceIntervalMs !== undefined ||
26807
- patch.message !== undefined ||
26808
- patch.parameters !== undefined;
26809
- if (hasSingleOnlyPatch) {
26810
- throw new PipelineExecutionError(_spaceTrim.spaceTrim(`
26811
- Bulk timeout update only supports the \`paused\` field.
26812
- `));
26813
- }
26814
- return {
26815
- allActive: true,
26816
- paused: patch.paused,
26817
- };
26818
- }
26819
- if (!timeoutId) {
26820
- throw new PipelineExecutionError(_spaceTrim.spaceTrim(`
26821
- Timeout \`timeoutId\` is required for single-timeout updates.
26822
- `));
26823
- }
26824
- if (Object.keys(patch).length === 0) {
26825
- throw new PipelineExecutionError(_spaceTrim.spaceTrim(`
26826
- Timeout update must include at least one editable field.
26827
- `));
26828
- }
26829
- return {
26830
- timeoutId,
26831
- patch,
26832
- };
26956
+ const target = parseTimeoutTargetSelection(args, {
26957
+ bothMessage: 'Timeout update must target either one `timeoutId` or `allActive: true`, not both.',
26958
+ missingMessage: 'Timeout update requires one `timeoutId` or `allActive: true`.',
26959
+ });
26960
+ const patch = parseTimeoutUpdatePatch(args);
26961
+ return target.allActive
26962
+ ? parseBulkTimeoutUpdateArgs(patch)
26963
+ : parseSingleTimeoutUpdateArgs(target.timeoutId, patch);
26833
26964
  },
26834
26965
  };
26835
26966