pomljs
Version:
Prompt Orchestration Markup Language
1,183 lines (1,182 loc) • 53.2 kB
JavaScript
import { parse } from '@xml-tools/parser';
import { buildAst } from '@xml-tools/ast';
import * as React from 'react';
import { ErrorCollection, StyleSheetProvider, SourceProvider, ReadError, findComponentByAlias, findComponentByAliasOrUndefined, listComponents } from './base.js';
import { deepMerge, readSource, parseText } from './util/index.js';
import { x as xmlContentAssistExports } from './_virtual/xmlContentAssist.js';
import { existsSync, readFileSync } from './util/fs.js';
import path__default from 'path';
import { POML_VERSION } from './version.js';
import { Schema, ToolsSchema } from './util/schema.js';
import { z } from 'zod';
/**
* Handles the files (with .poml extension).
*/
class PomlFile {
text;
sourcePath;
config;
ast;
cst;
tokenVector;
documentRange;
disabledComponents = new Set();
expressionTokens = [];
expressionEvaluations = new Map();
responseSchema;
toolsSchema;
runtimeParameters;
constructor(text, options, sourcePath) {
this.config = {
trim: options?.trim ?? true,
autoAddPoml: options?.autoAddPoml ?? true,
crlfToLf: options?.crlfToLf ?? true
};
this.text = this.config.crlfToLf ? text.replace(/\r\n/g, '\n') : text;
this.sourcePath = sourcePath;
if (this.sourcePath) {
const envFile = this.sourcePath.replace(/(source)?\.poml$/i, '.env');
if (existsSync(envFile)) {
try {
const envText = readFileSync(envFile, 'utf8');
const match = envText.match(/^SOURCE_PATH=(.*)$/m);
if (match) {
// The real source path is specified in the .env file.
this.sourcePath = match[1];
}
}
catch {
/* ignore */
}
}
}
this.documentRange = { start: 0, end: text.length - 1 };
let { ast, cst, tokenVector, errors } = this.readXml(text);
let addPoml = undefined;
if (this.config.autoAddPoml && text.slice(5).toLowerCase() !== '<poml') {
if (!ast || !ast.rootElement) {
// Invalid XML. Treating it as a free text.
addPoml = '<poml syntax="text" whiteSpace="pre">';
}
else if (
// Valid XML, but contains e.g., multiple root elements.
(ast.rootElement.position.startOffset > 0 &&
!this.testAllCommentsAndSpace(0, ast.rootElement.position.startOffset - 1, tokenVector)) ||
(ast.rootElement.position.endOffset + 1 < text.length &&
!this.testAllCommentsAndSpace(ast.rootElement.position.endOffset + 1, text.length - 1, tokenVector))) {
addPoml = '<poml syntax="markdown">';
}
}
if (addPoml) {
this.documentRange = { start: addPoml.length, end: text.length - 1 + addPoml.length };
this.config.trim = options?.trim ?? false; // TODO: this is an ad-hoc fix.
let { ast, cst, tokenVector, errors } = this.readXml(addPoml + text + '</poml>');
this.ast = ast;
this.cst = cst;
this.tokenVector = tokenVector;
// Report errors
for (const error of errors) {
ErrorCollection.add(error);
}
}
else {
this.ast = ast;
this.cst = cst;
this.tokenVector = tokenVector;
for (const error of errors) {
ErrorCollection.add(error);
}
}
}
readXml(text) {
const { cst, tokenVector, lexErrors, parseErrors } = parse(text);
const errors = [];
for (const lexError of lexErrors) {
errors.push(this.formatError(lexError.message, {
start: this.ensureRange(lexError.offset),
end: this.ensureRange(lexError.offset)
}, lexErrors));
}
for (const parseError of parseErrors) {
let startOffset = parseError.token.startOffset;
let endOffset = parseError.token.endOffset;
if (isNaN(startOffset) &&
(endOffset === undefined || isNaN(endOffset)) &&
parseError.previousToken) {
startOffset = parseError.previousToken.endOffset;
endOffset = parseError.previousToken.endOffset;
}
errors.push(this.formatError(parseError.message, {
start: this.ensureRange(startOffset),
end: endOffset ? this.ensureRange(endOffset) : this.ensureRange(startOffset)
}, parseError));
}
let ast = undefined;
try {
ast = buildAst(cst, tokenVector);
}
catch (e) {
errors.push(this.formatError('Error building AST', {
start: this.ensureRange(0),
end: this.ensureRange(text.length)
}, e));
}
return { ast, cst, tokenVector, errors };
}
testAllCommentsAndSpace(startOffset, endOffset, tokens) {
// start to end, inclusive. It must not be in the middle of a token.
const tokensFiltered = tokens.filter((token) => token.startOffset >= startOffset && (token.endOffset ?? token.startOffset) <= endOffset);
return tokensFiltered.every((token) => {
if (token.tokenType.name === 'SEA_WS' || token.tokenType.name === 'Comment') {
return true;
}
else if (/^\s*$/.test(token.image)) {
return true;
}
return false;
});
}
readJsonElement(parent, tagName) {
const element = xmlElementContents(parent).filter(i => i.type === 'XMLElement' && i.name?.toLowerCase() === tagName.toLowerCase());
if (element.length === 0) {
return undefined;
}
if (element.length > 1) {
this.reportError(`Multiple ${tagName} element found.`, {
start: this.ensureRange(element[0].position.startOffset),
end: this.ensureRange(element[element.length - 1].position.endOffset)
});
return undefined;
}
const text = xmlElementText(element[0]);
try {
return JSON.parse(text);
}
catch (e) {
this.reportError(e !== undefined && e.message
? e.message
: `Error parsing JSON: ${text}`, this.xmlElementRange(element[0]), e);
return undefined;
}
}
getResponseSchema() {
return this.responseSchema;
}
getToolsSchema() {
return this.toolsSchema;
}
getRuntimeParameters() {
return this.runtimeParameters;
}
xmlRootElement() {
if (!this.ast || !this.ast.rootElement) {
this.reportError('Root element is invalid.', {
start: this.ensureRange(this.documentRange.start),
end: this.ensureRange(this.documentRange.end)
});
return undefined;
}
else {
return this.ast.rootElement;
}
}
react(context) {
this.expressionTokens = [];
this.expressionEvaluations.clear();
const rootElement = this.xmlRootElement();
if (rootElement) {
// See whether stylesheet and context is available
const stylesheet = this.readJsonElement(rootElement, 'stylesheet');
context = deepMerge(context || {}, this.readJsonElement(rootElement, 'context') || {});
let parsedElement = this.parseXmlElement(rootElement, context || {}, {});
if (stylesheet) {
parsedElement = React.createElement(StyleSheetProvider, { stylesheet }, parsedElement);
}
if (this.sourcePath) {
parsedElement = React.createElement(SourceProvider, { source: this.sourcePath }, parsedElement);
}
return parsedElement;
}
else {
return React.createElement(React.Fragment, null);
}
}
getHoverToken(offset) {
const realOffset = this.recoverPosition(offset);
if (!this.ast || !this.ast.rootElement) {
return undefined;
}
return this.findTokenInElement(this.ast.rootElement, realOffset);
}
getCompletions(offset) {
const realOffset = this.recoverPosition(offset);
if (!this.ast || !this.ast.rootElement) {
return [];
}
return xmlContentAssistExports.getSuggestions({
ast: this.ast,
cst: this.cst,
tokenVector: this.tokenVector,
offset: realOffset,
providers: {
// 1. There are more types(scenarios) of suggestions providers (see api.d.ts)
// 2. Multiple providers may be supplied for a single scenario.
elementName: [this.handleElementNameCompletion(realOffset)],
elementNameClose: [this.handleElementNameCloseCompletion(realOffset)],
attributeName: [this.handleAttributeNameCompletion(realOffset)],
attributeValue: [this.handleAttributeValueCompletion(realOffset)]
}
});
}
getExpressionTokens() {
if (this.expressionTokens.length > 0) {
return this.expressionTokens;
}
if (!this.ast || !this.ast.rootElement) {
return [];
}
const tokens = [];
const regex = /{{\s*(.+?)\s*}}(?!})/gm;
const visit = (element) => {
// Special handling for schema and tool definition elements with parser="eval"
const elementName = element.name?.toLowerCase();
if (elementName === 'output-schema' || elementName === 'outputschema' || elementName === 'tool-definition' || elementName === 'tool-def' || elementName === 'tooldef' || elementName === 'tool') {
const parserAttr = xmlAttribute(element, 'parser');
const text = xmlElementText(element).trim();
// Check if it's an expression (either explicit parser="eval" or auto-detected)
if (parserAttr?.value === 'eval' || (!parserAttr && !text.trim().startsWith('{'))) {
const position = this.xmlElementRange(element.textContents[0]);
tokens.push({
type: 'expression',
range: position,
expression: text.trim(),
});
}
}
// attributes
for (const attr of element.attributes) {
if (!attr.value) {
continue;
}
if (attr.key?.toLowerCase() === 'if' || attr.key?.toLowerCase() === 'for') {
tokens.push({
type: 'expression',
range: this.xmlAttributeValueRange(attr),
expression: attr.value
});
continue;
}
if (element.name?.toLowerCase() === 'let' && attr.key?.toLowerCase() === 'value') {
tokens.push({
type: 'expression',
range: this.xmlAttributeValueRange(attr),
expression: attr.value
});
continue;
}
const range = this.xmlAttributeValueRange(attr);
regex.lastIndex = 0;
let match;
while ((match = regex.exec(attr.value))) {
tokens.push({
type: 'expression',
range: {
start: range.start + match.index,
end: range.start + match.index + match[0].length - 1,
},
expression: match[1],
});
}
}
// text contents
for (const tc of element.textContents) {
const text = tc.text || '';
const pos = this.xmlElementRange(tc);
// Regular template expression handling for other elements and JSON
regex.lastIndex = 0;
let match;
while ((match = regex.exec(text))) {
tokens.push({
type: 'expression',
range: {
start: pos.start + match.index,
end: pos.start + match.index + match[0].length - 1,
},
expression: match[1],
});
}
}
for (const child of element.subElements) {
visit(child);
}
};
visit(this.ast.rootElement);
return tokens;
}
getExpressionEvaluations(range) {
const key = `${range.start}:${range.end}`;
return this.expressionEvaluations.get(key) ?? [];
}
recordEvaluation(range, output) {
const key = `${range.start}:${range.end}`;
const evaluationList = this.expressionEvaluations.get(key) ?? [];
evaluationList.push(output);
this.expressionEvaluations.set(key, evaluationList);
}
formatError(msg, range, cause) {
return ReadError.fromProps(msg, {
originalStartIndex: range?.start,
originalEndIndex: range?.end,
sourcePath: this.sourcePath
}, { cause: cause });
}
reportError(msg, range, cause) {
ErrorCollection.add(this.formatError(msg, range, cause));
}
/**
* Template related functions only usable in standalone poml files.
* It's not available for POML expressed with JSX or Python SDK.
*/
handleForLoop(element, context) {
const forLoop = element.attributes.find(attr => attr.key?.toLowerCase() === 'for');
if (!forLoop) {
// No for loop found.
return undefined;
}
const forLoopValue = forLoop.value;
if (!forLoopValue) {
this.reportError('for attribute value is expected.', this.xmlElementRange(element));
return undefined;
}
const [itemName, listName] = forLoopValue.match(/(.+)\s+in\s+(.+)/)?.slice(1) || [null, null];
if (!itemName || !listName) {
this.reportError('item in list syntax is expected in for attribute.', this.xmlAttributeValueRange(forLoop));
return undefined;
}
const list = this.evaluateExpression(listName, context, this.xmlAttributeValueRange(forLoop));
if (!Array.isArray(list)) {
this.reportError('List is expected in for attribute.', this.xmlAttributeValueRange(forLoop));
return undefined;
}
return list.map((item, index) => {
const loop = {
index: index,
length: list.length,
first: index === 0,
last: index === list.length - 1
};
return { loop: loop, [itemName]: item };
});
}
handleIfCondition = (element, context) => {
const ifCondition = element.attributes.find(attr => attr.key?.toLowerCase() === 'if');
if (!ifCondition) {
// No if condition found.
return true;
}
const ifConditionValue = ifCondition.value;
if (!ifConditionValue) {
this.reportError('if attribute value is expected.', this.xmlAttributeValueRange(ifCondition));
return false;
}
const condition = this.evaluateExpression(ifConditionValue, context, this.xmlAttributeValueRange(ifCondition), true);
if (condition) {
return true;
}
else {
return false;
}
};
handleLet = (element, contextIn, contextOut) => {
if (element.name?.toLowerCase() !== 'let') {
return false;
}
const source = xmlAttribute(element, 'src')?.value;
const type = xmlAttribute(element, 'type')?.value;
const name = xmlAttribute(element, 'name')?.value;
const value = xmlAttribute(element, 'value')?.value;
// Case 1: <let name="var1" src="/path/to/file" />, case insensitive
// or <let src="/path/to/file" />, case insensitive
if (source) {
let content;
try {
content = readSource(source, this.sourcePath ? path__default.dirname(this.sourcePath) : undefined, type);
}
catch (e) {
this.reportError(e !== undefined && e.message
? e.message
: `Error reading source: ${source}`, this.xmlAttributeValueRange(xmlAttribute(element, 'src')), e);
return true;
}
if (!name) {
if (content && typeof content === 'object') {
Object.assign(contextOut, content);
}
else {
this.reportError('name attribute is expected when the source is not an object.', this.xmlElementRange(element));
}
}
else {
contextOut[name] = content;
}
return true;
}
// Case 2: <let name="var1" value="{{ expression }}" />, case insensitive
if (value) {
if (!name) {
this.reportError('name attribute is expected when <let> contains two attributes.', this.xmlElementRange(element));
return true;
}
const evaluated = this.evaluateExpression(value, contextIn, this.xmlAttributeValueRange(xmlAttribute(element, 'value')), true);
contextOut[name] = evaluated;
return true;
}
// Case 3: <let>{ JSON }</let>
// or <let name="var1" type="number">{ JSON }</let>
if (element.textContents.length > 0) {
const text = xmlElementText(element);
let content;
try {
content = parseText(text, type);
}
catch (e) {
this.reportError(e !== undefined && e.message
? e.message
: `Error parsing text as type ${type}: ${text}`, this.xmlElementRange(element), e);
return true;
}
if (!name) {
if (content && typeof content === 'object') {
Object.assign(contextOut, content);
}
else {
this.reportError('name attribute is expected when the source is not an object.', this.xmlElementRange(element));
}
}
else {
contextOut[name] = content;
}
return true;
}
this.reportError('Invalid <let> element.', this.xmlElementRange(element));
return true;
};
handleAttribute = (attribute, context) => {
if (!attribute.key || !attribute.value) {
return;
}
if (attribute.key.toLowerCase() === 'for' || attribute.key.toLowerCase() === 'if') {
return;
}
const key = hyphenToCamelCase(attribute.key);
const value = this.handleText(attribute.value, context, this.xmlAttributeValueRange(attribute));
if (value.length === 1) {
return [key, value[0]];
}
else {
return [key, value];
}
};
handleInclude = (element, context) => {
if (element.name?.toLowerCase() !== 'include') {
return undefined;
}
const src = xmlAttribute(element, 'src');
if (!src || !src.value) {
this.reportError('src attribute is expected.', this.xmlElementRange(element));
return React.createElement(React.Fragment, null);
}
const source = src.value;
let text;
try {
text = readSource(source, this.sourcePath ? path__default.dirname(this.sourcePath) : undefined, 'string');
}
catch (e) {
this.reportError(e !== undefined && e.message
? e.message
: `Error reading source: ${source}`, this.xmlAttributeValueRange(src), e);
return React.createElement(React.Fragment, null);
}
const includePath = this.sourcePath && !path__default.isAbsolute(source)
? path__default.join(path__default.dirname(this.sourcePath), source)
: source;
const included = new PomlFile(text, this.config, includePath);
const root = included.xmlRootElement();
if (!root) {
return React.createElement(React.Fragment, null);
}
let contents = [];
if (root.name?.toLowerCase() === 'poml') {
contents = xmlElementContents(root);
}
else {
contents = [root];
}
const resultNodes = [];
contents.forEach((el, idx) => {
if (el.type === 'XMLTextContent') {
resultNodes.push(...included
.handleText(el.text ?? '', context, included.xmlElementRange(el))
.map(v => typeof v === 'object' && v !== null && !React.isValidElement(v)
? JSON.stringify(v)
: v));
}
else if (el.type === 'XMLElement') {
const child = included.parseXmlElement(el, context, {});
resultNodes.push(React.isValidElement(child) ? React.cloneElement(child, { key: `child-${idx}` }) : child);
}
});
if (resultNodes.length === 1) {
return React.createElement(React.Fragment, null, resultNodes[0]);
}
return React.createElement(React.Fragment, null, resultNodes);
};
handleSchema = (element, context) => {
const parserAttr = xmlAttribute(element, 'parser');
let parser = parserAttr?.value
? this.handleTextAsString(parserAttr.value, context || {}, this.xmlAttributeValueRange(parserAttr))
: undefined;
const text = xmlElementText(element).trim();
// Get the range for the text content (if available)
const textRange = element.textContents.length > 0
? this.xmlElementRange(element.textContents[0])
: this.xmlElementRange(element);
// Auto-detect parser if not specified
if (!parser) {
if (text.startsWith('{')) {
parser = 'json';
}
else {
parser = 'eval';
}
}
else if (parser !== 'json' && parser !== 'eval') {
this.reportError(`Invalid parser attribute: ${parser}. Expected "json" or "eval"`, this.xmlAttributeValueRange(xmlAttribute(element, 'parser')));
return undefined;
}
try {
if (parser === 'json') {
// Process template expressions in JSON text
const processedText = this.handleText(text, context || {}, textRange);
// handleText returns an array, join if all strings
const jsonText = processedText.length === 1 && typeof processedText[0] === 'string'
? processedText[0]
: processedText.map(p => typeof p === 'string' ? p : JSON.stringify(p)).join('');
const jsonSchema = JSON.parse(jsonText);
return Schema.fromOpenAPI(jsonSchema);
}
else if (parser === 'eval') {
// Evaluate expression directly with z in context
const contextWithZ = { z, ...context };
const result = this.evaluateExpression(text, contextWithZ, textRange, true);
// If evaluation failed, result will be empty string
if (!result) {
return undefined;
}
// Determine if result is a Zod schema or JSON schema
if (result && typeof result === 'object' && result._def) {
// It's a Zod schema
return Schema.fromZod(result);
}
else {
// Treat as JSON schema
return Schema.fromOpenAPI(result);
}
}
}
catch (e) {
this.reportError(e instanceof Error ? e.message : 'Error parsing schema', this.xmlElementRange(element), e);
}
return undefined;
};
handleOutputSchema = (element, context) => {
const elementName = element.name?.toLowerCase();
if (elementName !== 'output-schema' && elementName !== 'outputschema') {
return false;
}
if (this.responseSchema) {
this.reportError('Multiple output-schema elements found. Only one is allowed.', this.xmlElementRange(element));
return true;
}
const schema = this.handleSchema(element, context);
if (schema) {
this.responseSchema = schema;
}
return true;
};
handleToolDefinition = (element, context) => {
const elementName = element.name?.toLowerCase();
if (elementName !== 'tool-definition' && elementName !== 'tool-def' && elementName !== 'tooldef' && elementName !== 'tool') {
return false;
}
const nameAttr = xmlAttribute(element, 'name');
if (!nameAttr?.value) {
this.reportError('name attribute is required for tool definition', this.xmlElementRange(element));
return true;
}
// Process template expressions in name attribute
const name = this.handleTextAsString(nameAttr.value, context || {}, this.xmlAttributeValueRange(nameAttr));
const descriptionAttr = xmlAttribute(element, 'description');
// Process template expressions in description attribute if present
const description = descriptionAttr?.value
? this.handleTextAsString(descriptionAttr.value, context || {}, this.xmlAttributeValueRange(descriptionAttr))
: undefined;
const inputSchema = this.handleSchema(element, context);
if (inputSchema) {
if (!this.toolsSchema) {
this.toolsSchema = new ToolsSchema();
}
try {
this.toolsSchema.addTool(name, description || undefined, inputSchema);
}
catch (e) {
this.reportError(e instanceof Error ? e.message : 'Error adding tool to tools schema', this.xmlElementRange(element), e);
}
}
return true;
};
handleRuntime = (element, context) => {
const elementName = element.name?.toLowerCase();
if (elementName !== 'runtime') {
return false;
}
// Extract runtime parameters from all attributes
const runtimeParams = {};
for (const attribute of element.attributes) {
if (attribute.key && attribute.value) {
// Process template expressions and convert to string
const stringValue = this.handleTextAsString(attribute.value, context || {}, this.xmlAttributeValueRange(attribute));
// Convert key to camelCase (kebab-case to camelCase)
const camelKey = hyphenToCamelCase(attribute.key);
// Convert value (auto-convert booleans, numbers, JSON)
const convertedValue = this.convertRuntimeValue(stringValue);
runtimeParams[camelKey] = convertedValue;
}
}
this.runtimeParameters = runtimeParams;
return true;
};
convertRuntimeValue = (value) => {
// Convert boolean-like values
if (value === 'true') {
return true;
}
if (value === 'false') {
return false;
}
// Convert number-like values
if (/^-?\d*\.?\d+$/.test(value)) {
const num = parseFloat(value);
if (!isNaN(num)) {
return num;
}
}
// Convert JSON-like values (arrays and objects)
if ((value.startsWith('[') && value.endsWith(']')) ||
(value.startsWith('{') && value.endsWith('}'))) {
try {
return JSON.parse(value);
}
catch {
// If JSON parsing fails, return as string
return value;
}
}
// Return as string for everything else
return value;
};
handleMeta = (element, context) => {
if (element.name?.toLowerCase() !== 'meta') {
return false;
}
// Check if this is an old-style meta with type attribute
const metaType = xmlAttribute(element, 'type')?.value;
if (metaType) {
this.reportError(`Meta elements with type attribute have been removed. Use <${metaType === 'schema' ? 'output-schema' : metaType === 'tool' ? 'tool-definition' : metaType === 'runtime' ? 'runtime' : metaType}> instead of <meta type="${metaType}">`, this.xmlElementRange(element));
return true;
}
// Handle version control
const minVersion = xmlAttribute(element, 'minVersion')?.value;
if (minVersion && compareVersions(POML_VERSION, minVersion) < 0) {
this.reportError(`POML version ${minVersion} or higher is required`, this.xmlAttributeValueRange(xmlAttribute(element, 'minVersion')));
}
const maxVersion = xmlAttribute(element, 'maxVersion')?.value;
if (maxVersion && compareVersions(POML_VERSION, maxVersion) > 0) {
this.reportError(`POML version ${maxVersion} or lower is required`, this.xmlAttributeValueRange(xmlAttribute(element, 'maxVersion')));
}
const comps = xmlAttribute(element, 'components')?.value;
if (comps) {
comps.split(/[,\s]+/).forEach(token => {
token = token.trim();
if (!token) {
return;
}
const op = token[0];
const name = token.slice(1).toLowerCase().trim();
if (!name) {
return;
}
if (op === '+') {
this.disabledComponents.delete(name);
}
else if (op === '-') {
this.disabledComponents.add(name);
}
else {
this.reportError(`Invalid component operation: ${op}. Use + to enable or - to disable.`, this.xmlAttributeValueRange(xmlAttribute(element, 'components')));
}
});
}
return true;
};
unescapeText = (text) => {
return text
.replace(/#lt;/g, '<')
.replace(/#gt;/g, '>')
.replace(/#amp;/g, '&')
.replace(/#quot;/g, '"')
.replace(/#apos;/g, "'")
.replace(/#hash;/g, '#')
.replace(/#lbrace;/g, '{')
.replace(/#rbrace;/g, '}');
};
handleText = (text, context, position) => {
let curlyMatch;
let replacedPrefixLength = 0;
let results = [];
const regex = /{{\s*(.+?)\s*}}(?!})/gm;
while ((curlyMatch = regex.exec(text))) {
const curlyExpression = curlyMatch[1];
const value = this.evaluateExpression(curlyExpression, context, position
? {
start: position.start + curlyMatch.index,
end: position.start + curlyMatch.index + curlyMatch[0].length - 1
}
: undefined);
if (this.config.trim && curlyMatch[0] === text.trim()) {
return [value];
}
if (curlyMatch.index > replacedPrefixLength) {
results.push(this.unescapeText(text.slice(replacedPrefixLength, curlyMatch.index)));
}
results.push(value);
replacedPrefixLength = curlyMatch.index + curlyMatch[0].length;
}
if (text.length > replacedPrefixLength) {
results.push(this.unescapeText(text.slice(replacedPrefixLength)));
}
if (results.length > 0 && results.every(r => typeof r === 'string' || typeof r === 'number')) {
return [results.map(r => r.toString()).join('')];
}
return results;
};
handleTextAsString = (text, context, position) => {
const results = this.handleText(text, context, position);
if (results.length === 1) {
if (typeof results[0] === 'string') {
return results[0];
}
else {
return results[0].toString();
}
}
else {
return JSON.stringify(results);
}
};
evaluateExpression(expression, context, range, stripCurlyBrackets = false) {
try {
if (stripCurlyBrackets) {
const curlyMatch = expression.match(/^\s*{{\s*(.+?)\s*}}\s*$/m);
if (curlyMatch) {
expression = curlyMatch[1];
}
}
const result = evalWithVariables(expression, context || {});
if (range) {
this.recordEvaluation(range, result);
}
return result;
}
catch (e) {
const errMessage = e !== undefined && e.message
? e.message
: `Error evaluating expression: ${expression}`;
if (range) {
this.recordEvaluation(range, errMessage);
}
this.reportError(errMessage, range, e);
return '';
}
}
/**
* Parse the XML element and return the corresponding React element.
*
* @param element The element to be converted.
* @param globalContext The context can be carried over when the function returns.
* @param localContext The context that is only available in the current element and its children.
*/
parseXmlElement(element, globalContext, localContext) {
// Let. Always set the global.
if (this.handleLet(element, { ...globalContext, ...localContext }, globalContext)) {
return React.createElement(React.Fragment, null);
}
const tagName = element.name;
if (!tagName) {
// Probably already had an invalid syntax error.
return React.createElement(React.Fragment, null);
}
const tagNameLower = tagName.toLowerCase();
const isMeta = tagNameLower === 'meta';
const isInclude = tagNameLower === 'include';
const isOutputSchema = tagNameLower === 'output-schema' || tagNameLower === 'outputschema';
const isToolDefinition = tagNameLower === 'tool-definition' || tagNameLower === 'tool-def' || tagNameLower === 'tooldef' || tagNameLower === 'tool';
const isRuntime = tagNameLower === 'runtime';
// Common logic for handling for-loops
const forLoops = this.handleForLoop(element, { ...globalContext, ...localContext });
const forLoopedContext = forLoops === undefined ? [{}] : forLoops;
const resultElements = [];
for (let i = 0; i < forLoopedContext.length; i++) {
const currentLocal = { ...localContext, ...forLoopedContext[i] };
const context = { ...globalContext, ...currentLocal };
// Common logic for handling if-conditions
if (!this.handleIfCondition(element, context)) {
continue;
}
// Common logic for handling meta elements and new schema/tool elements
if (isMeta && this.handleMeta(element, context)) {
// If it's a meta element, we don't render anything.
continue;
}
if (isOutputSchema && this.handleOutputSchema(element, context)) {
// If it's an output-schema element, we don't render anything.
continue;
}
if (isToolDefinition && this.handleToolDefinition(element, context)) {
// If it's a tool-definition element, we don't render anything.
continue;
}
if (isRuntime && this.handleRuntime(element, context)) {
// If it's a runtime element, we don't render anything.
continue;
}
let elementToAdd = null;
if (isInclude) {
// Logic for <include> tags
const included = this.handleInclude(element, context);
if (included) {
// Add a key if we are in a loop with multiple items
if (forLoopedContext.length > 1) {
elementToAdd = React.createElement(React.Fragment, { key: `include-${i}` }, included);
}
else {
elementToAdd = included;
}
}
}
else {
// Logic for all other components
const component = findComponentByAlias(tagName, this.disabledComponents);
if (typeof component === 'string') {
// Add a read error
this.reportError(component, this.xmlOpenNameRange(element));
// Return empty fragment to prevent rendering this element
// You might want to 'continue' the loop as well.
return React.createElement(React.Fragment, null);
}
const attrib = element.attributes.reduce((acc, attribute) => {
const [key, value] = this.handleAttribute(attribute, context) || [null, null];
if (key && value !== null) {
acc[key] = value;
}
return acc;
}, {});
// Retain the position of current element for future diagnostics
const range = this.xmlElementRange(element);
attrib.originalStartIndex = range.start;
attrib.originalEndIndex = range.end;
// Add key attribute for react
if (!attrib.key && forLoopedContext.length > 1) {
attrib.key = `key-${i}`;
}
const contents = xmlElementContents(element).filter(el => {
// Filter out stylesheet and context element in the root poml element
if (tagName === 'poml' &&
el.type === 'XMLElement' &&
['context', 'stylesheet'].includes(el.name?.toLowerCase() ?? '')) {
return false;
}
else {
return true;
}
});
const avoidObject = (el) => {
if (typeof el === 'object' && el !== null && !React.isValidElement(el)) {
return JSON.stringify(el);
}
return el;
};
const processedContents = contents.reduce((acc, el, i) => {
if (el.type === 'XMLTextContent') {
// const isFirst = i === 0,
// isLast = i === contents.length - 1;
// const text = this.config.trim ? trimText(el.text || '', isFirst, isLast) : el.text || '';
acc.push(...this.handleText(el.text ?? '', { ...globalContext, ...currentLocal }, this.xmlElementRange(el)).map(avoidObject));
}
else if (el.type === 'XMLElement') {
acc.push(this.parseXmlElement(el, globalContext, currentLocal));
}
return acc;
}, []);
elementToAdd = React.createElement(component.render.bind(component), attrib, ...processedContents);
}
if (elementToAdd) {
// If we have an element to add, push it to the result elements.
resultElements.push(elementToAdd);
}
}
// Common logic for returning the final result
if (resultElements.length === 1) {
return resultElements[0];
}
else {
// Cases where there are multiple elements or zero elements.
return React.createElement(React.Fragment, null, resultElements);
}
}
recoverPosition(position) {
return position + this.documentRange.start;
}
ensureRange(position) {
return Math.max(Math.min(position, this.documentRange.end) - this.documentRange.start, 0);
}
xmlElementRange(element) {
return {
start: this.ensureRange(element.position.startOffset),
end: this.ensureRange(element.position.endOffset)
};
}
xmlOpenNameRange(element) {
if (element.syntax.openName) {
return {
start: this.ensureRange(element.syntax.openName.startOffset),
end: this.ensureRange(element.syntax.openName.endOffset)
};
}
else {
return this.xmlElementRange(element);
}
}
xmlCloseNameRange(element) {
if (element.syntax.closeName) {
return {
start: this.ensureRange(element.syntax.closeName.startOffset),
end: this.ensureRange(element.syntax.closeName.endOffset)
};
}
else {
return this.xmlElementRange(element);
}
}
xmlAttributeKeyRange(element) {
if (element.syntax.key) {
return {
start: this.ensureRange(element.syntax.key.startOffset),
end: this.ensureRange(element.syntax.key.endOffset)
};
}
else {
return this.xmlElementRange(element);
}
}
xmlAttributeValueRange(element) {
if (element.syntax.value) {
return {
start: this.ensureRange(element.syntax.value.startOffset),
end: this.ensureRange(element.syntax.value.endOffset)
};
}
else {
return this.xmlElementRange(element);
}
}
findTokenInElement(element, offset) {
if (element.name) {
if (element.syntax.openName &&
element.syntax.openName.startOffset <= offset &&
offset <= element.syntax.openName.endOffset) {
return {
type: 'element',
range: this.xmlOpenNameRange(element),
element: element.name
};
}
if (element.syntax.closeName &&
element.syntax.closeName.startOffset <= offset &&
offset <= element.syntax.closeName.endOffset) {
return {
type: 'element',
range: this.xmlCloseNameRange(element),
element: element.name
};
}
for (const attrib of element.attributes) {
if (attrib.key &&
attrib.syntax.key &&
attrib.syntax.key.startOffset <= offset &&
offset <= attrib.syntax.key.endOffset) {
return {
type: 'attribute',
range: this.xmlAttributeKeyRange(attrib),
element: element.name,
attribute: attrib.key
};
}
}
}
for (const child of element.subElements) {
const result = this.findTokenInElement(child, offset);
if (result) {
return result;
}
}
}
handleElementNameCompletion(offset) {
return ({ element, prefix }) => {
const candidates = this.findComponentWithPrefix(prefix, true);
return candidates.map(candidate => {
return {
type: 'element',
range: {
start: this.ensureRange(offset - (prefix ? prefix.length : 0)),
end: this.ensureRange(offset - 1)
},
element: candidate
};
});
};
}
handleElementNameCloseCompletion(offset) {
return ({ element, prefix }) => {
const candidates = [];
const excludedComponents = [];
if (element.name) {
candidates.push(element.name);
const component = findComponentByAliasOrUndefined(element.name, this.disabledComponents);
if (component !== undefined) {
excludedComponents.push(component.name);
}
}
if (prefix) {
candidates.push(...this.findComponentWithPrefix(prefix, true, excludedComponents));
}
return candidates.map(candidate => {
return {
type: 'element',
range: {
start: this.ensureRange(offset - (prefix ? prefix.length : 0)),
end: this.ensureRange(offset - 1)
},
element: candidate
};
});
};
}
handleAttributeNameCompletion(offset) {
return ({ element, prefix }) => {
if (!element.name) {
return [];
}
const component = findComponentByAliasOrUndefined(element.name, this.disabledComponents);
const parameters = component?.parameters();
if (!component || !parameters) {
return [];
}
const candidates = [];
for (const parameter of parameters) {
if (parameter.name.toLowerCase().startsWith(prefix?.toLowerCase() ?? '')) {
candidates.push({
type: 'attribute',
range: {
start: this.ensureRange(offset - (prefix ? prefix.length : 0)),
end: this.ensureRange(offset - 1)
},
element: component.name,
attribute: parameter.name
});
}
}
return candidates;
};
}
handleAttributeValueCompletion(offset) {
return ({ element, attribute, prefix }) => {
if (!element.name) {
return [];
}
const component = findComponentByAliasOrUndefined(element.name, this.disabledComponents);
const parameters = component?.parameters();
if (!component || !parameters) {
return [];
}
const candidates = [];
for (const parameter of parameters) {
if (parameter.name.toLowerCase() === attribute.key?.toLowerCase()) {
for (const choice of parameter.choices) {
if (choice.toLowerCase().startsWith(prefix?.toLowerCase() ?? '')) {
candidates.push({
type: 'attributeValue',
range: {
start: this.ensureRange(offset - (prefix ? prefix.length : 0)),
end: this.ensureRange(offset - 1)
},
element: component.name,
attribute: parameter.name,
value: choice
});
}
}
}
}
return candidates;
};
}
findComponentWithPrefix(prefix, publicOnly, excludedComponents) {
const candidates = [];
for (const component of listComponents()) {
if (publicOnly && !component.isPublic()) {
continue;
}
if (excludedComponents && excludedComponents.includes(component.name)) {
continue;
}
let nameMatch = undefined;
if (!prefix || component.name.toLowerCase().startsWith(prefix.toLowerCase())) {
nameMatch = component.name;
}
else {
const candidates = [];
for (const alias of component.getAliases()) {
if (alias.toLowerCase().startsWith(prefix.toLowerCase())) {
candidates.push(alias);
// One component can have at most one alias match.
break;
}
}
// Match hyphen case.
for (const alias of component.getAliases(false)) {
const aliasHyphen = camelToHyphenCase(alias);
if (aliasHyphen.startsWith(prefix.toLowerCase())) {
candidates.push(aliasHyphen);
break;
}
}
// Try to see if there is a match in the exact case.
for (const candidate of candidates) {
if (candidate.startsWith(prefix)) {
nameMatch = candidate;
break;
}
}
if (!nameMatch && candidates) {
nameMatch = candidates[0];