pomljs
Version:
Prompt Orchestration Markup Language
629 lines (626 loc) • 27.5 kB
JavaScript
import * as React from 'react';
import { component, ReadError, expandRelative, useWithCatch } from './base.js';
import { Serialize, Free, Markup, MultiMedia, computePresentationOrUndefined } from './presentation.js';
import * as fs from 'fs';
import { preprocessImage } from './util/image.js';
import { preprocessAudio } from './util/audio.js';
const FREE_SYNTAXES = ['text'];
const MARKUP_SYNTAXES = ['markdown', 'html', 'csv', 'tsv'];
const SERIALIZE_SYNTAXES = ['json', 'yaml', 'xml'];
const MULTIMEDIA_SYNTAXES = ['multimedia'];
const computeSyntaxContext = (props, defaultSyntax, invalidPresentations) => {
const { syntax, ...others } = props;
invalidPresentations = invalidPresentations ?? ['multimedia'];
// 1. Create the full presentation style based on the syntax shortcut.
// This is the case when syntax is explicity specified.
let presentationStyle;
if (!syntax) {
presentationStyle = {};
}
else if (MARKUP_SYNTAXES.includes(syntax)) {
if (invalidPresentations.includes('markup')) {
throw ReadError.fromProps(`Markup syntax (${syntax}) is not supported here.`, others);
}
presentationStyle = { presentation: 'markup', markupLang: syntax };
}
else if (SERIALIZE_SYNTAXES.includes(syntax)) {
if (invalidPresentations.includes('serialize')) {
throw ReadError.fromProps(`Serialize syntax (${syntax}) is not supported here.`, others);
}
presentationStyle = { presentation: 'serialize', serializer: syntax };
}
else if (FREE_SYNTAXES.includes(syntax)) {
if (invalidPresentations.includes('free')) {
throw ReadError.fromProps(`Free syntax (${syntax}) is not supported here.`, others);
}
presentationStyle = { presentation: 'free' };
}
else if (MULTIMEDIA_SYNTAXES.includes(syntax)) {
if (invalidPresentations.includes('multimedia')) {
throw ReadError.fromProps(`Multimedia syntax (${syntax}) is not supported here.`, others);
}
presentationStyle = { presentation: 'multimedia' };
}
else {
throw ReadError.fromProps(`Unsupported syntax: ${syntax}`, others);
}
// 2. Compute the presentation context.
// Try to inherit presentation and syntax from parents.
// There are two cases where the inherited presentation does not count.
// (a) No presentation is found.
// (b) The presentation is free and the syntax is not specified.
const presentation = computePresentationOrUndefined(presentationStyle);
if (!presentation || (presentation === 'free' && !syntax)) {
if (syntax) {
// This should not happen. Must be a bug.
throw ReadError.fromProps(`Syntax is specified (${syntax}) but presentation method is not found. Something is wrong.`, others);
}
// Try again with a default syntax
return computeSyntaxContext({ ...others, syntax: defaultSyntax || 'markdown' }, defaultSyntax, invalidPresentations);
}
return presentation;
};
// Helper component for contents that are designed for markup, but also work in other syntaxes.
const AnyOrFree = component('AnyOrFree')((props) => {
const { syntax, children, presentation, name, type, asAny, ...others } = props;
if (presentation === 'serialize') {
if (asAny) {
return (React.createElement(Serialize.Any, { serializer: syntax, name: name, type: type, ...others }, children));
}
else {
return (React.createElement(Serialize.Environment, { serializer: syntax, ...others }, children));
}
}
else if (presentation === 'free') {
return React.createElement(Free.Text, { ...others }, children);
}
else {
throw ReadError.fromProps(`This component is not designed for ${presentation} syntaxes.`, others);
}
});
/**
* Text (`<text>`, `<poml>`) is a wrapper for any contents.
* By default, it uses `markdown` syntax and writes the contents within it directly to the output.
* When used with "markup" syntaxes, it renders a standalone section preceded and followed by one blank line.
* It's mostly used in the root element of a prompt, but it should also work in any other places.
* This component will be automatically added as a wrapping root element if it's not provided:
* 1. If the first element is pure text contents, `<poml syntax="text">` will be added.
* 2. If the first element is a POML component, `<poml syntax="markdown">` will be added.
*
* @param {'markdown'|'html'|'json'|'yaml'|'xml'|'text'} syntax - The syntax of the content. Note `xml` and `text` are experimental.
* @param className - A class name for quickly styling the current block with stylesheets.
* @param {'human'|'ai'|'system'} speaker - The speaker of the content. By default, it's determined by the context and the content.
* @param name - The name of the content, used in serialization.
* @param type - The type of the content, used in serialization.
* @param {object} writerOptions - **Experimental.**. Optional JSON string to customize the format of markdown headers, JSON indents, etc.
* @param {'pre'|'filter'|'trim'} whiteSpace - **Experimental.** Controls how whitespace is handled in text content.
* `'pre'` (default when `syntax` is `text`): Preserves all whitespace as-is;
* `'filter'` (default when `syntax` is not `text`): Removes leading/trailing whitespace and normalizes internal whitespace in the gaps;
* `'trim'`: Trims whitespace from the beginning and end.
* @param {number} charLimit - **Experimental.** Soft character limit before truncation is applied. Content exceeding this limit will be truncated with a marker.
* @param {number} tokenLimit - **Experimental.** Soft token limit before truncation is applied. Content exceeding this limit will be truncated with a marker.
* @param {number} priority - **Experimental.** Priority used when truncating globally. Lower numbers are dropped first when content needs to be reduced to fit limits.
*
* @example
* ```xml
* <poml syntax="text">
* Contents of the whole prompt.
*
* 1. Your customized list.
* 2. You don't need to know anything about POML.
* </poml>
* ```
*
* To render the whole prompt in markdown syntax with a "human" speaker:
*
* ```xml
* <poml syntax="markdown" speaker="human">
* <p>You are a helpful assistant.</p>
* <p>What is the capital of France?</p>
* </poml>
* ```
*
* **Experimental usage with limits and priority:**
*
* ```xml
* <poml syntax="markdown" tokenLimit="10">
* <p priority="1">This has lower priority and may be truncated first.</p>
* <p priority="3">This has higher priority and will be preserved longer.</p>
* </poml>
* ```
*/
const Text = component('Text', ['div', 'poml'])((props) => {
const { syntax, children, name, type, ...others } = props;
const presentation = computeSyntaxContext(props, 'markdown');
if (presentation === 'markup') {
return (React.createElement(Paragraph, { syntax: syntax, blankLine: false, ...others }, children));
}
else {
return (React.createElement(AnyOrFree, { syntax: syntax, presentation: presentation, asAny: true, name: name, type: type, ...others }, children));
}
});
/**
* Paragraph (`<p>`) is a standalone section preceded by and followed by two blank lines in markup syntaxes.
* It's mostly used for text contents.
*
* @param {boolean} blankLine - Whether to add one more blank line (2 in total) before and after the paragraph.
*
* @see {@link Text} for other props available.
*
* @example
* ```xml
* <p>Contents of the paragraph.</p>
* ```
*/
const Paragraph = component('Paragraph', ['p'])((props) => {
const { syntax, children, name, type, ...others } = props;
const presentation = computeSyntaxContext(props);
if (presentation === 'markup') {
return (React.createElement(Markup.Paragraph, { markupLang: syntax, ...others }, children));
}
else {
return (React.createElement(AnyOrFree, { syntax: syntax, presentation: presentation, asAny: true, name: name, type: type, ...others }, children));
}
});
/**
* Inline (`<span>`) is a container for inline content.
* When used with markup syntaxes, it wraps text in an inline style, without any preceding or following blank characters.
* In serializer syntaxes, it's treated as a generic value.
* Inline elements are not designed to be used alone (especially in serializer syntaxes).
* One might notice problematic renderings (e.g., speaker not applied) when using it alone.
*
* @param {'markdown'|'html'|'json'|'yaml'|'xml'|'text'} syntax - The syntax of the content.
* @param className - A class name for quickly styling the current block with stylesheets.
* @param {'human'|'ai'|'system'} speaker - The speaker of the content. By default, it's determined by the context and the content.
* @param {object} writerOptions - **Experimental.**. Optional JSON string to customize the format of markdown headers, JSON indents, etc.
* @param {'pre'|'filter'|'trim'} whiteSpace - **Experimental.** Controls how whitespace is handled in text content.
* `'pre'` (default when `syntax` is `text`): Preserves all whitespace as-is;
* `'filter'` (default when `syntax` is not `text`): Removes leading/trailing whitespace and normalizes internal whitespace in the gaps;
* `'trim'`: Trims whitespace from the beginning and end.
* @param {number} charLimit - **Experimental.** Soft character limit before truncation is applied. Content exceeding this limit will be truncated with a marker.
* @param {number} tokenLimit - **Experimental.** Soft token limit before truncation is applied. Content exceeding this limit will be truncated with a marker.
* @param {number} priority - **Experimental.** Priority used when truncating globally. Lower numbers are dropped first when content needs to be reduced to fit limits.
*
* @example
* ```xml
* <p>I'm listening to <span>music</span> right now.</p>
* ```
*/
const Inline = component('Inline', ['span'])((props) => {
const { syntax, children, ...others } = props;
const presentation = computeSyntaxContext(props);
if (presentation === 'markup') {
return (React.createElement(Markup.Inline, { markupLang: syntax, ...others }, children));
}
else {
return (React.createElement(AnyOrFree, { syntax: syntax, presentation: presentation, asAny: false, ...others }, children));
}
});
/**
* Newline (`<br>`) explicitly adds a line break, primarily in markup syntaxes.
* In serializer syntaxes, it's ignored.
*
* @param {number} newLineCount - The number of linebreaks to add.
*
* @see {@link Inline} for other props available.
*
* @example
* ```xml
* <br />
* ```
*/
const Newline = component('Newline', ['br'])((props) => {
const { syntax, ...others } = props;
const presentation = computeSyntaxContext(props);
if (presentation === 'markup') {
return React.createElement(Markup.Newline, { markupLang: syntax, ...others });
}
else {
return null;
}
});
/**
* Header (`<h>`) renders headings in markup syntaxes.
* It's commonly used to highlight titles or section headings.
* The header level will be automatically computed based on the context.
* Use SubContent (`<section>`) for nested content.
*
* @see {@link Paragraph} for other props available.
*
* @example
* ```xml
* <Header syntax="markdown">Section Title</Header>
* ```
*/
const Header = component('Header', ['h'])((props) => {
const { syntax, children, name, type, ...others } = props;
const presentation = computeSyntaxContext(props);
if (presentation === 'markup') {
return (React.createElement(Markup.Header, { markupLang: syntax, ...others }, children));
}
else {
return (React.createElement(AnyOrFree, { syntax: syntax, presentation: presentation, asAny: true, name: name, type: type, ...others }, children));
}
});
/**
* SubContent (`<section>`) renders nested content, often following a header.
* The headers within the section will be automatically adjusted to a lower level.
*
* @see {@link Paragraph} for other props available.
*
* @example
* ```xml
* <h>Section Title</h>
* <section>
* <h>Sub-section Title</h> <!-- Nested header -->
* <p>Sub-section details</p>
* </section>
* ```
*/
const SubContent = component('SubContent', ['section'])((props) => {
const { syntax, children, name, type, ...others } = props;
const presentation = computeSyntaxContext(props);
if (presentation === 'markup') {
return (React.createElement(Markup.SubContent, { markupLang: syntax, ...others }, children));
}
else {
return (React.createElement(AnyOrFree, { syntax: syntax, presentation: presentation, asAny: true, name: name, type: type, ...others }, children));
}
});
/**
* Bold (`<b>`) emphasizes text in a bold style when using markup syntaxes.
*
* @see {@link Inline} for other props available.
*
* @example
* ```xml
* <p><b>Task:</b> Do something.</p>
* ```
*/
const Bold = component('Bold', ['b'])((props) => {
const { syntax, children, ...others } = props;
const presentation = computeSyntaxContext(props);
if (presentation === 'markup') {
return (React.createElement(Markup.Bold, { markupLang: syntax, ...others }, children));
}
else {
return (React.createElement(AnyOrFree, { syntax: syntax, presentation: presentation, asAny: false, ...others }, children));
}
});
/**
* Italic (`<i>`) emphasizes text in an italic style when using markup syntaxes.
*
* @see {@link Inline} for other props available.
*
* @example
* ```xml
* Your <i>italicized</i> text.
* ```
*/
const Italic = component('Italic', ['i'])((props) => {
const { syntax, children, ...others } = props;
const presentation = computeSyntaxContext(props);
if (presentation === 'markup') {
return (React.createElement(Markup.Italic, { markupLang: syntax, ...others }, children));
}
else {
return (React.createElement(AnyOrFree, { syntax: syntax, presentation: presentation, asAny: false, ...others }, children));
}
});
/**
* Strikethrough (`<s>`, `<strike>`) indicates removed or invalid text in markup syntaxes.
*
* @see {@link Inline} for other props available.
*
* @example
* ```xml
* <s>This messages is removed.</s>
* ```
*/
component('Strikethrough', ['s', 'strike'])((props) => {
const { syntax, children, ...others } = props;
const presentation = computeSyntaxContext(props);
if (presentation === 'markup') {
return (React.createElement(Markup.Strikethrough, { markupLang: syntax, ...others }, children));
}
else {
return (React.createElement(AnyOrFree, { syntax: syntax, presentation: presentation, asAny: false, ...others }, children));
}
});
/**
* Underline (`<u>`) draws a line beneath text in markup syntaxes.
*
* @see {@link Inline} for other props available.
*
* @example
* ```xml
* This text is <u>underlined</u>.
* ```
*/
component('Underline', ['u'])((props) => {
const { syntax, children, ...others } = props;
const presentation = computeSyntaxContext(props);
if (presentation === 'markup') {
return (React.createElement(Markup.Underline, { markupLang: syntax, ...others }, children));
}
else {
return (React.createElement(AnyOrFree, { syntax: syntax, presentation: presentation, asAny: false, ...others }, children));
}
});
/**
* Code is used to represent code snippets or inline code in markup syntaxes.
*
* @param {boolean} inline - Whether to render code inline or as a block. Default is `true`.
* @param lang - The language of the code snippet.
*
* @see {@link Paragraph} for other props available.
*
* @example
* ```xml
* <code inline="true">const x = 42;</code>
* ```
*
* ```xml
* <code lang="javascript">
* const x = 42;
* </code>
* ```
*/
const Code = component('Code')((props) => {
const { syntax, children, name, type, ...others } = props;
const presentation = computeSyntaxContext(props);
if (presentation === 'markup') {
return (React.createElement(Markup.Code, { markupLang: syntax, ...others }, children));
}
else {
return (React.createElement(AnyOrFree, { syntax: syntax, presentation: presentation, asAny: true, name: name, type: type, ...others }, children));
}
});
/**
* List (`<list>`) is a container for multiple ListItem (`<item>`) elements.
* When used with markup syntaxes, a bullet or numbering is added.
*
* @param {'star'|'dash'|'plus'|'decimal'|'latin'} listStyle - The style for the list marker, such as dash or star. Default is `dash`.
*
* @see {@link Paragraph} for other props available.
*
* @example
* ```xml
* <list listStyle="decimal">
* <item>Item 1</item>
* <item>Item 2</item>
* </list>
* ```
*/
const List = component('List')((props) => {
const { syntax, children, listStyle, name, type, ...others } = props;
const presentation = computeSyntaxContext(props);
if (presentation === 'markup') {
return (React.createElement(Markup.List, { markupLang: syntax, listStyle: listStyle, ...others }, children));
}
else {
return (React.createElement(AnyOrFree, { syntax: syntax, presentation: presentation, asAny: true, name: name, type: type ?? 'array', ...others }, children));
}
});
/**
* ListItem (`<item>`) is an item within a List component.
* In markup mode, it is rendered with the specified bullet or numbering style.
*
* @see {@link Paragraph} for other props available.
*
* @example
* ```xml
* <list listStyle="decimal">
* <item blankLine="true">Item 1</item>
* <item>Item 2</item>
* </list>
* ```
*/
const ListItem = component('ListItem', ['item'])((props) => {
const { syntax, children, name, type, ...others } = props;
const presentation = computeSyntaxContext(props);
if (presentation === 'markup') {
return (React.createElement(Markup.ListItem, { markupLang: syntax, ...others }, children));
}
else {
return (React.createElement(AnyOrFree, { syntax: syntax, presentation: presentation, asAny: true, name: name, type: type, ...others }, children));
}
});
/**
* Object (`<obj>`, `<dataObj>`) displays external data or object content.
* When in serialize mode, it's serialized according to the given serializer.
*
* @param {'markdown'|'html'|'json'|'yaml'|'xml'} syntax - The syntax or serializer of the content. Default is `json`.
* @param {object} data - The data object to render.
*
* @see {@link Inline} for other props available.
*
* @example
* ```xml
* <Object syntax="json" data="{ key: 'value' }" />
* ```
*/
const Object$1 = component('Object', ['obj', 'dataObj'])((props) => {
const { syntax, children, ...others } = props;
const presentation = computeSyntaxContext(props, 'json');
if (presentation === 'serialize') {
return (React.createElement(Serialize.Object, { serializer: syntax, ...others }, children));
}
else {
return React.createElement(Text, { syntax: syntax }, JSON.stringify(props.data));
}
});
/**
* Image (`<img>`) displays an image in the content.
* Alternatively, it can also be shown as an alt text by specifying the `syntax` prop.
* Note that syntax must be specified as `multimedia` to show the image.
*
* @see {@link Inline} for other props available.
*
* @param {string} src - The path to the image file.
* @param {string} alt - The alternative text to show when the image cannot be displayed.
* @param {string} base64 - The base64 encoded image data. It can not be specified together with `src`.
* @param {string} type - The MIME type of the image **to be shown**. If not specified, it will be inferred from the file extension.
* If specified, the image will be converted to the specified type. Can be `image/jpeg`, `image/png`, etc., or without the `image/` prefix.
* @param {'top'|'bottom'|'here'} position - The position of the image. Default is `here`.
* @param {number} maxWidth - The maximum width of the image to be shown.
* @param {number} maxHeight - The maximum height of the image to be shown.
* @param {number} resize - The ratio to resize the image to to be shown.
* @param {'markdown'|'html'|'json'|'yaml'|'xml'|'multimedia'} syntax - Only when specified as `multimedia`, the image will be shown.
* Otherwise, the alt text will be shown. By default, it's `multimedia` when `alt` is not specified. Otherwise, it's undefined (inherit from parent).
*
* @example
* ```xml
* <Image src="path/to/image.jpg" alt="Image description" position="bottom" />
* ```
*/
const Image = component('Image', { aliases: ['img'], asynchorous: true })((props) => {
let { syntax, src, base64, alt, type, position, maxWidth, maxHeight, resize, ...others } = props;
if (!alt) {
syntax = syntax ?? 'multimedia';
}
const presentation = computeSyntaxContext({ ...props, syntax }, 'multimedia', []);
if (presentation === 'multimedia') {
if (src) {
if (base64) {
throw ReadError.fromProps('Cannot specify both `src` and `base64`.', others);
}
src = expandRelative(src);
if (!fs.existsSync(src)) {
throw ReadError.fromProps(`Image file not found: ${src}`, others);
}
}
else if (!base64) {
throw ReadError.fromProps('Either `src` or `base64` must be specified.', others);
}
const image = useWithCatch(preprocessImage({ src, base64, type, maxWidth, maxHeight, resize }), others);
if (!image) {
return null;
}
return (React.createElement(MultiMedia.Image, { presentation: presentation, base64: image.base64, position: position, type: image.mimeType, alt: alt, ...others }));
}
else {
return React.createElement(Inline, { syntax: syntax }, alt);
}
});
/**
* Audio (`<audio>`) embeds an audio file in the content.
*
* Accepts either a file path (`src`) or base64-encoded audio data (`base64`).
* The MIME type can be provided via `type` or will be inferred from the file extension.
*
* @param {string} src - Path to the audio file. If provided, the file will be read and encoded as base64.
* @param {string} base64 - Base64-encoded audio data. Cannot be used together with `src`.
* @param {string} alt - The alternative text to show when the image cannot be displayed.
* @param {string} type - The MIME type of the audio (e.g., audio/mpeg, audio/wav). If not specified, it will be inferred from the file extension.
* The type must be consistent with the real type of the file. The consistency will NOT be checked or converted.
* The type can be specified with or without the `audio/` prefix.
* @param {'top'|'bottom'|'here'} position - The position of the image. Default is `here`.
* @param {'markdown'|'html'|'json'|'yaml'|'xml'|'multimedia'} syntax - Only when specified as `multimedia`, the image will be shown.
* Otherwise, the alt text will be shown. By default, it's `multimedia` when `alt` is not specified. Otherwise, it's undefined (inherit from parent).
*
* @example
* ```xml
* <Audio src="path/to/audio.mp3" />
* ```
* @example
* ```xml
* <Audio base64="..." type="audio/wav" />
* ```
*/
component('Audio', { aliases: ['audio'], asynchorous: true })((props) => {
let { syntax, src, base64, type, ...others } = props;
const presentation = computeSyntaxContext(props, 'multimedia', []);
if (presentation === 'multimedia') {
if (src) {
if (base64) {
throw ReadError.fromProps('Cannot specify both `src` and `base64`.', others);
}
src = expandRelative(src);
if (!fs.existsSync(src)) {
throw ReadError.fromProps(`Audio file not found: ${src}`, others);
}
}
else if (!base64) {
throw ReadError.fromProps('Either `src` or `base64` must be specified.', others);
}
const audio = useWithCatch(preprocessAudio({ src, base64, type }), others);
if (!audio) {
return null;
}
return (React.createElement(MultiMedia.Audio, { presentation: presentation, base64: audio.base64, type: audio.mimeType, ...others }));
}
else {
return null;
}
});
/**
* ToolRequest represents an AI-generated tool request with parameters.
* Used to display tool calls made by AI models.
*
* @param {string} id - Tool request ID
* @param {string} name - Tool name
* @param {any} parameters - Tool input parameters
* @param {'human'|'ai'|'system'} speaker - The speaker of the content. Default is `ai`.
*
* @example
* ```xml
* <ToolRequest id="123" name="search" parameters={{ query: "hello" }} />
* ```
*/
const ToolRequest = component('ToolRequest', { aliases: ['toolRequest'] })((props) => {
let { syntax, id, name, parameters, speaker, ...others } = props;
syntax = syntax ?? 'multimedia';
const presentation = computeSyntaxContext({ ...props, syntax }, 'multimedia', []);
if (presentation === 'multimedia') {
return (React.createElement(MultiMedia.ToolRequest, { presentation: presentation, id: id, name: name, parameters: parameters, speaker: speaker ?? 'ai', ...others }));
}
else {
return (React.createElement(Object$1, { syntax: syntax, speaker: speaker ?? 'ai', data: { id, name, parameters }, ...others }));
}
});
/**
* ToolResponse represents the result of a tool execution.
* Used to display tool execution results with rich content.
*
* @param {'markdown'|'html'|'json'|'yaml'|'xml'|'text'} syntax - The syntax of ToolResponse is special.
* It is always `multimedia` for itself. The syntax is used to render the content inside.
* If not specified, it will inherit from the parent context.
* @param {string} id - Tool call ID to respond to
* @param {string} name - Tool name
* @param {'human'|'ai'|'system'|'tool'} speaker - The speaker of the content. Default is `tool`.
*
* @example
* ```xml
* <ToolResponse id="123" name="search">
* <Paragraph>Search results for "hello":</Paragraph>
* <List>
* <ListItem>Result 1</ListItem>
* <ListItem>Result 2</ListItem>
* </List>
* </ToolResponse>
* ```
*/
const ToolResponse = component('ToolResponse', { aliases: ['toolResponse'] })((props) => {
const { syntax, id, name, children, speaker, ...others } = props;
const presentation = computeSyntaxContext(props);
let syntaxFromContext = syntax;
if (syntaxFromContext === undefined) {
if (presentation === 'markup') {
syntaxFromContext = 'markdown';
}
else if (presentation === 'serialize') {
syntaxFromContext = 'json';
}
else if (presentation === 'free') {
syntaxFromContext = 'text';
}
else if (presentation === 'multimedia') {
syntaxFromContext = 'multimedia';
}
}
return (React.createElement(MultiMedia.ToolResponse, { presentation: 'multimedia', id: id, name: name, speaker: speaker ?? 'tool', ...others },
React.createElement(Inline, { syntax: syntaxFromContext }, children)));
});
export { AnyOrFree, Bold, Code, Header, Image, Inline, Italic, List, ListItem, Newline, Object$1 as Object, Paragraph, SubContent, Text, ToolRequest, ToolResponse, computeSyntaxContext };
//# sourceMappingURL=essentials.js.map