sf-agent-framework
Version:
AI Agent Orchestration Framework for Salesforce Development - Two-phase architecture with 70% context reduction
611 lines (505 loc) ⢠17.1 kB
JavaScript
/**
* Enhanced Template Processing Engine for SF-Agent Framework
* Processes templates with embedded LLM instructions, variables, and conditional logic
* Based on advanced template architecture patterns
*/
const fs = require('fs-extra');
const path = require('path');
const yaml = require('js-yaml');
const chalk = require('chalk');
class TemplateProcessor {
constructor(options = {}) {
this.options = {
templateDir: options.templateDir || 'sf-core/templates',
outputDir: options.outputDir || 'docs/generated',
variablePrefix: options.variablePrefix || '{{',
variableSuffix: options.variableSuffix || '}}',
llmPrefix: options.llmPrefix || '[[LLM:',
llmSuffix: options.llmSuffix || ']]',
verbose: options.verbose || false,
...options,
};
this.variables = {};
this.context = {};
this.llmInstructions = [];
this.processedSections = [];
}
/**
* Process a template file with full LLM instruction support
*/
async processTemplate(templatePath, variables = {}, context = {}) {
console.log(chalk.blue(`\nš Processing template: ${templatePath}`));
try {
// Load template
const template = await this.loadTemplate(templatePath);
// Set variables and context
this.variables = { ...this.variables, ...variables };
this.context = { ...this.context, ...context };
// Process template sections
const output = await this.processTemplateSections(template);
// Save output
const outputPath = await this.saveOutput(template, output);
console.log(chalk.green(`ā
Template processed successfully`));
console.log(chalk.blue(`š Output: ${outputPath}`));
return {
success: true,
outputPath,
llmInstructions: this.llmInstructions,
processedSections: this.processedSections,
};
} catch (error) {
console.error(chalk.red(`ā Error processing template: ${error.message}`));
throw error;
}
}
/**
* Load and parse template YAML
*/
async loadTemplate(templatePath) {
const content = await fs.readFile(templatePath, 'utf-8');
const template = yaml.load(content);
// Validate template structure
this.validateTemplate(template);
return template;
}
/**
* Validate template has required structure
*/
validateTemplate(template) {
const required = ['template', 'sections'];
for (const field of required) {
if (!template[field]) {
throw new Error(`Template missing required field: ${field}`);
}
}
if (!template.template.id || !template.template.name) {
throw new Error('Template must have id and name');
}
if (!Array.isArray(template.sections)) {
throw new Error('Template sections must be an array');
}
}
/**
* Process all template sections
*/
async processTemplateSections(template) {
const output = [];
const metadata = template.template;
// Add template header
output.push(this.generateHeader(metadata));
// Process each section
for (const section of template.sections) {
if (this.shouldProcessSection(section)) {
const processedSection = await this.processSection(section, 1);
output.push(processedSection);
this.processedSections.push({
id: section.id,
title: section.title,
processed: true,
});
} else {
this.processedSections.push({
id: section.id,
title: section.title,
processed: false,
reason: 'Condition not met',
});
}
}
return output.join('\n\n');
}
/**
* Check if section should be processed based on conditions
*/
shouldProcessSection(section) {
if (!section.condition) return true;
// Evaluate condition
return this.evaluateCondition(section.condition);
}
/**
* Evaluate conditional expression
*/
evaluateCondition(condition) {
// Simple condition evaluation
// Format: "variable_name == value" or "variable_name != value"
if (typeof condition === 'boolean') return condition;
if (typeof condition !== 'string') return true;
// Parse condition
const matches = condition.match(/(\w+)\s*(==|!=|>|<|>=|<=)\s*(.+)/);
if (!matches) return true;
const [, varName, operator, value] = matches;
const varValue = this.getVariable(varName);
switch (operator) {
case '==':
return String(varValue) === value.replace(/['"]/g, '');
case '!=':
return String(varValue) !== value.replace(/['"]/g, '');
case '>':
return Number(varValue) > Number(value);
case '<':
return Number(varValue) < Number(value);
case '>=':
return Number(varValue) >= Number(value);
case '<=':
return Number(varValue) <= Number(value);
default:
return true;
}
}
/**
* Process a single section with all its content
*/
async processSection(section, level = 1) {
const output = [];
// Add section title
const titlePrefix = '#'.repeat(level + 1);
output.push(`${titlePrefix} ${section.title}`);
// Process LLM instruction if present
if (section.instruction) {
const instruction = this.processLLMInstruction(section.instruction, section);
if (this.options.verbose) {
output.push(`<!-- LLM Instruction: ${instruction} -->`);
}
this.llmInstructions.push({
section: section.id,
instruction: instruction,
});
}
// Process static content
if (section.content) {
output.push(this.processContent(section.content));
}
// Process subsections
if (section.subsections && Array.isArray(section.subsections)) {
for (const subsection of section.subsections) {
if (this.shouldProcessSection(subsection)) {
const subOutput = await this.processSection(subsection, level + 1);
output.push(subOutput);
}
}
}
// Process repeatable sections
if (section.repeatable && section.repeat_for) {
const items = this.getVariable(section.repeat_for);
if (Array.isArray(items)) {
for (const item of items) {
// Set loop variable
this.variables[section.loop_var || 'item'] = item;
// Process repeated content
const repeatedContent = this.processContent(section.repeat_template || section.content);
output.push(repeatedContent);
}
}
}
// Process examples (never included in output, for guidance only)
if (section.examples && this.options.includeExamples) {
output.push('\n**Examples:**');
for (const example of section.examples) {
output.push(`- ${example}`);
}
}
return output.join('\n\n');
}
/**
* Process LLM instruction
*/
processLLMInstruction(instruction, section) {
// Replace variables in instruction
let processed = this.replaceVariables(instruction);
// Add context if available
if (section.context_required) {
processed = `[Context: ${section.context_required}] ${processed}`;
}
// Mark as LLM instruction
processed = `${this.options.llmPrefix} ${processed} ${this.options.llmSuffix}`;
return processed;
}
/**
* Process content with variable substitution
*/
processContent(content) {
if (!content) return '';
// Replace variables
let processed = this.replaceVariables(content);
// Process inline LLM instructions
processed = this.processInlineLLMInstructions(processed);
// Process conditional blocks
processed = this.processConditionalBlocks(processed);
return processed;
}
/**
* Replace variables in content
*/
replaceVariables(content) {
const regex = new RegExp(
`${this.escapeRegex(this.options.variablePrefix)}([^${this.escapeRegex(this.options.variableSuffix)}]+)${this.escapeRegex(this.options.variableSuffix)}`,
'g'
);
return content.replace(regex, (match, varName) => {
const value = this.getVariable(varName.trim());
return value !== undefined ? value : match;
});
}
/**
* Get variable value with dot notation support
*/
getVariable(varName) {
// Support dot notation (e.g., user.name)
const parts = varName.split('.');
let value = this.variables;
for (const part of parts) {
if (value && typeof value === 'object') {
value = value[part];
} else {
return undefined;
}
}
// Check context if not found in variables
if (value === undefined) {
value = this.context[varName];
}
return value;
}
/**
* Process inline LLM instructions in content
*/
processInlineLLMInstructions(content) {
const regex = new RegExp(
`${this.escapeRegex(this.options.llmPrefix)}([^${this.escapeRegex(this.options.llmSuffix)}]+)${this.escapeRegex(this.options.llmSuffix)}`,
'g'
);
const matches = content.matchAll(regex);
for (const match of matches) {
this.llmInstructions.push({
type: 'inline',
instruction: match[1].trim(),
position: match.index,
});
}
// Remove LLM instructions from output unless in verbose mode
if (!this.options.verbose) {
content = content.replace(regex, '');
}
return content;
}
/**
* Process conditional blocks in content
*/
processConditionalBlocks(content) {
// Process IF blocks
const ifRegex = /\[\[IF:\s*([^\]]+)\]\]([\s\S]*?)\[\[ENDIF\]\]/g;
content = content.replace(ifRegex, (match, condition, block) => {
if (this.evaluateCondition(condition)) {
return block;
}
return '';
});
// Process IF-ELSE blocks
const ifElseRegex = /\[\[IF:\s*([^\]]+)\]\]([\s\S]*?)\[\[ELSE\]\]([\s\S]*?)\[\[ENDIF\]\]/g;
content = content.replace(ifElseRegex, (match, condition, ifBlock, elseBlock) => {
if (this.evaluateCondition(condition)) {
return ifBlock;
}
return elseBlock;
});
return content;
}
/**
* Generate template header
*/
generateHeader(metadata) {
const header = [];
header.push(`# ${metadata.name}`);
if (metadata.description) {
header.push(`\n${metadata.description}`);
}
if (metadata.version) {
header.push(`\n**Version:** ${metadata.version}`);
}
header.push(`\n**Generated:** ${new Date().toISOString()}`);
if (this.options.verbose && Object.keys(this.variables).length > 0) {
header.push('\n## Variables Used');
for (const [key, value] of Object.entries(this.variables)) {
header.push(`- ${key}: ${JSON.stringify(value)}`);
}
}
return header.join('\n');
}
/**
* Save processed output
*/
async saveOutput(template, output) {
const metadata = template.template;
// Determine output path
let outputPath;
if (metadata.output && metadata.output.location) {
outputPath = path.join(metadata.output.location, metadata.output.naming || 'output.md');
} else {
outputPath = path.join(this.options.outputDir, `${metadata.id}.md`);
}
// Replace variables in output path
outputPath = this.replaceVariables(outputPath);
// Ensure directory exists
await fs.ensureDir(path.dirname(outputPath));
// Write file
await fs.writeFile(outputPath, output);
return outputPath;
}
/**
* Escape regex special characters
*/
escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Process multiple templates
*/
async processTemplates(templates, variables = {}, context = {}) {
const results = [];
for (const templatePath of templates) {
const result = await this.processTemplate(templatePath, variables, context);
results.push(result);
}
return results;
}
/**
* Get template metadata without processing
*/
async getTemplateMetadata(templatePath) {
const template = await this.loadTemplate(templatePath);
return {
id: template.template.id,
name: template.template.name,
version: template.template.version,
description: template.template.description,
sections: template.sections.map((s) => ({
id: s.id,
title: s.title,
required: s.required || false,
condition: s.condition || null,
})),
};
}
/**
* Validate variables against template requirements
*/
async validateVariables(templatePath, variables) {
const template = await this.loadTemplate(templatePath);
const errors = [];
const warnings = [];
// Check required variables
if (template.variables) {
for (const varDef of template.variables) {
if (typeof varDef === 'string') {
if (!variables[varDef]) {
warnings.push(`Variable '${varDef}' is not provided`);
}
} else if (typeof varDef === 'object') {
const varName = varDef.name || Object.keys(varDef)[0];
const required = varDef.required !== false;
if (required && !variables[varName]) {
errors.push(`Required variable '${varName}' is missing`);
}
}
}
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
}
// CLI Interface
if (require.main === module) {
const program = require('commander');
program.version('1.0.0').description('Process SF-Agent templates with LLM instructions');
program
.command('process <template>')
.description('Process a template file')
.option('-v, --variables <json>', 'Variables as JSON string')
.option('-f, --var-file <file>', 'Variables from JSON file')
.option('-o, --output <dir>', 'Output directory')
.option('--verbose', 'Include LLM instructions in output')
.action(async (template, options) => {
try {
const processor = new TemplateProcessor({
outputDir: options.output,
verbose: options.verbose,
});
// Load variables
let variables = {};
if (options.variables) {
variables = JSON.parse(options.variables);
} else if (options.varFile) {
variables = await fs.readJson(options.varFile);
}
// Process template
const result = await processor.processTemplate(template, variables);
if (options.verbose) {
console.log(chalk.yellow('\nLLM Instructions:'));
for (const inst of result.llmInstructions) {
console.log(` - ${inst.section}: ${inst.instruction}`);
}
}
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
process.exit(1);
}
});
program
.command('validate <template>')
.description('Validate template structure')
.option('-v, --variables <json>', 'Check variables')
.action(async (template, options) => {
try {
const processor = new TemplateProcessor();
// Get metadata
const metadata = await processor.getTemplateMetadata(template);
console.log(chalk.blue('\nTemplate Metadata:'));
console.log(JSON.stringify(metadata, null, 2));
// Validate variables if provided
if (options.variables) {
const variables = JSON.parse(options.variables);
const validation = await processor.validateVariables(template, variables);
if (validation.valid) {
console.log(chalk.green('\nā
Variables valid'));
} else {
console.log(chalk.red('\nā Variable errors:'));
validation.errors.forEach((e) => console.log(` - ${e}`));
}
if (validation.warnings.length > 0) {
console.log(chalk.yellow('\nā ļø Warnings:'));
validation.warnings.forEach((w) => console.log(` - ${w}`));
}
}
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
process.exit(1);
}
});
program
.command('batch <pattern>')
.description('Process multiple templates matching pattern')
.option('-v, --variables <json>', 'Variables as JSON string')
.option('-o, --output <dir>', 'Output directory')
.action(async (pattern, options) => {
try {
const glob = require('glob');
const templates = glob.sync(pattern);
console.log(chalk.blue(`Found ${templates.length} templates`));
const processor = new TemplateProcessor({
outputDir: options.output,
});
const variables = options.variables ? JSON.parse(options.variables) : {};
const results = await processor.processTemplates(templates, variables);
console.log(chalk.green(`\nā
Processed ${results.length} templates`));
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
process.exit(1);
}
});
program.parse();
}
module.exports = TemplateProcessor;