UNPKG

pomljs

Version:

Prompt Orchestration Markup Language

1,183 lines (1,182 loc) 53.2 kB
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];