UNPKG

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
#!/usr/bin/env node /** * 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;