UNPKG

sf-agent-framework

Version:

AI Agent Orchestration Framework for Salesforce Development - Two-phase architecture with 70% context reduction

663 lines (571 loc) • 18.9 kB
#!/usr/bin/env node /** * Interactive Workflow Builder for SF-Agent Framework * Enables dynamic workflow execution with user choice points, branching logic, and validation gates */ const fs = require('fs-extra'); const path = require('path'); const yaml = require('js-yaml'); const chalk = require('chalk'); const inquirer = require('inquirer'); class InteractiveWorkflowBuilder { constructor(options = {}) { this.options = { workflowDir: options.workflowDir || 'sf-core/workflows', outputDir: options.outputDir || 'docs/workflow-sessions', templateDir: options.templateDir || 'sf-core/templates', verbose: options.verbose || false, ...options, }; this.currentWorkflow = null; this.workflowState = { phase: 'initialization', completedSteps: [], artifacts: {}, decisions: {}, validationResults: {}, context: {}, }; this.choiceHistory = []; this.branchPath = []; } /** * Start an interactive workflow session */ async startWorkflow(workflowId) { console.log(chalk.blue(`\nšŸš€ Starting Interactive Workflow: ${workflowId}`)); try { // Load workflow definition const workflowPath = path.join(this.options.workflowDir, `${workflowId}.yaml`); const workflowContent = await fs.readFile(workflowPath, 'utf-8'); this.currentWorkflow = yaml.load(workflowContent); // Validate workflow structure this.validateWorkflow(this.currentWorkflow); // Initialize workflow session await this.initializeSession(); // Execute workflow phases await this.executeWorkflow(); // Save session results await this.saveSession(); console.log(chalk.green('\nāœ… Workflow completed successfully!')); return this.workflowState; } catch (error) { console.error(chalk.red(`\nāŒ Workflow error: ${error.message}`)); throw error; } } /** * Validate workflow has required interactive elements */ validateWorkflow(workflow) { if (!workflow.workflow) { throw new Error('Invalid workflow: missing workflow definition'); } const required = ['id', 'name', 'phases']; for (const field of required) { if (!workflow.workflow[field]) { throw new Error(`Workflow missing required field: ${field}`); } } } /** * Initialize workflow session */ async initializeSession() { console.log(chalk.cyan('\nšŸ“‹ Initializing workflow session...')); // Gather initial context const { projectType } = await inquirer.prompt([ { type: 'list', name: 'projectType', message: 'What type of Salesforce project are you working on?', choices: [ { name: 'New Org Implementation (Greenfield)', value: 'greenfield' }, { name: 'Existing Org Enhancement (Brownfield)', value: 'brownfield' }, { name: 'Data Migration Project', value: 'migration' }, { name: 'Integration Development', value: 'integration' }, { name: 'Package Development', value: 'package' }, { name: 'Other/Custom', value: 'custom' }, ], }, ]); this.workflowState.context.projectType = projectType; // Get project complexity const { complexity } = await inquirer.prompt([ { type: 'list', name: 'complexity', message: 'What is the project complexity?', choices: [ { name: 'Simple (< 10 user stories)', value: 'simple' }, { name: 'Medium (10-50 user stories)', value: 'medium' }, { name: 'Complex (50-200 user stories)', value: 'complex' }, { name: 'Enterprise (200+ user stories)', value: 'enterprise' }, ], }, ]); this.workflowState.context.complexity = complexity; // Ask about specific needs const { needs } = await inquirer.prompt([ { type: 'checkbox', name: 'needs', message: 'Select all that apply to your project:', choices: [ { name: 'Requirements Gathering', value: 'requirements' }, { name: 'Architecture Design', value: 'architecture' }, { name: 'Data Model Design', value: 'datamodel' }, { name: 'Integration Design', value: 'integration' }, { name: 'Security Design', value: 'security' }, { name: 'UI/UX Design', value: 'uiux' }, { name: 'Development Planning', value: 'development' }, { name: 'Testing Strategy', value: 'testing' }, { name: 'Deployment Planning', value: 'deployment' }, ], }, ]); this.workflowState.context.needs = needs; } /** * Execute workflow with interactive decision points */ async executeWorkflow() { const phases = this.currentWorkflow.workflow.phases; for (const phase of phases) { console.log(chalk.blue(`\nšŸ“ Phase: ${phase.name}`)); // Check if phase has prerequisites if (phase.prerequisites) { const canProceed = await this.checkPrerequisites(phase.prerequisites); if (!canProceed) { const { skipOrResolve } = await inquirer.prompt([ { type: 'list', name: 'skipOrResolve', message: `Prerequisites not met for ${phase.name}. What would you like to do?`, choices: [ { name: 'Resolve prerequisites first', value: 'resolve' }, { name: 'Skip this phase', value: 'skip' }, { name: 'Continue anyway (not recommended)', value: 'continue' }, ], }, ]); if (skipOrResolve === 'skip') { console.log(chalk.yellow(`ā­ļø Skipping phase: ${phase.name}`)); continue; } else if (skipOrResolve === 'resolve') { await this.resolvePrerequisites(phase.prerequisites); } } } // Execute phase based on type if (phase.interactive) { await this.executeInteractivePhase(phase); } else if (phase.conditional) { await this.executeConditionalPhase(phase); } else if (phase.parallel) { await this.executeParallelPhase(phase); } else { await this.executeSequentialPhase(phase); } // Phase validation gate if (phase.validation_gate) { await this.executeValidationGate(phase); } this.workflowState.phase = phase.name; } } /** * Execute interactive phase with user choices */ async executeInteractivePhase(phase) { console.log(chalk.cyan(`\nšŸŽÆ Interactive Phase: ${phase.name}`)); for (const step of phase.steps) { if (step.user_choice) { const choice = await this.presentUserChoice(step); this.choiceHistory.push({ step: step.id, choice }); // Execute branch based on choice const branch = step.branches[choice]; if (branch) { await this.executeBranch(branch); } } else if (step.optional) { const { execute } = await inquirer.prompt([ { type: 'confirm', name: 'execute', message: `Would you like to execute: ${step.description}?`, default: step.recommended || false, }, ]); if (execute) { await this.executeStep(step); } } else { await this.executeStep(step); } } } /** * Present user choice and get selection */ async presentUserChoice(step) { const choices = step.choices.map((choice) => ({ name: `${choice.label}${choice.recommended ? ' (Recommended)' : ''}`, value: choice.value, short: choice.value, })); const { selection } = await inquirer.prompt([ { type: 'list', name: 'selection', message: step.question || `Choose an option for ${step.name}:`, choices, }, ]); this.workflowState.decisions[step.id] = selection; return selection; } /** * Execute conditional phase based on context */ async executeConditionalPhase(phase) { console.log(chalk.cyan(`\nšŸ”€ Conditional Phase: ${phase.name}`)); for (const condition of phase.conditions) { if (await this.evaluateCondition(condition)) { console.log(chalk.green(`āœ“ Condition met: ${condition.description}`)); await this.executeBranch(condition.then_branch); return; } } // Execute default branch if no conditions met if (phase.default_branch) { console.log(chalk.yellow('→ Executing default branch')); await this.executeBranch(phase.default_branch); } } /** * Execute parallel phase with track selection */ async executeParallelPhase(phase) { console.log(chalk.cyan(`\n⚔ Parallel Phase: ${phase.name}`)); const { tracks } = await inquirer.prompt([ { type: 'checkbox', name: 'tracks', message: 'Select which tracks to execute in parallel:', choices: phase.tracks.map((track) => ({ name: track.name, value: track.id, checked: track.default !== false, })), }, ]); // Execute selected tracks for (const trackId of tracks) { const track = phase.tracks.find((t) => t.id === trackId); console.log(chalk.blue(`\nšŸ“Œ Track: ${track.name}`)); for (const step of track.steps) { await this.executeStep(step); } } } /** * Execute sequential phase */ async executeSequentialPhase(phase) { console.log(chalk.cyan(`\nāž”ļø Sequential Phase: ${phase.name}`)); for (const step of phase.steps) { await this.executeStep(step); } } /** * Execute individual step */ async executeStep(step) { console.log(chalk.gray(` • Executing: ${step.name || step.action}`)); // Simulate step execution if (step.agent) { console.log(chalk.gray(` Agent: ${step.agent}`)); } if (step.task) { console.log(chalk.gray(` Task: ${step.task}`)); } if (step.creates) { const artifacts = Array.isArray(step.creates) ? step.creates : [step.creates]; for (const artifact of artifacts) { this.workflowState.artifacts[artifact] = { created_by: step.agent || 'system', created_at: new Date().toISOString(), step: step.name, }; console.log(chalk.green(` āœ“ Created: ${artifact}`)); } } this.workflowState.completedSteps.push({ name: step.name, agent: step.agent, timestamp: new Date().toISOString(), }); } /** * Execute branch */ async executeBranch(branch) { this.branchPath.push(branch.id || branch.name); if (branch.steps) { for (const step of branch.steps) { await this.executeStep(step); } } if (branch.nested_choice) { const choice = await this.presentUserChoice(branch.nested_choice); const nestedBranch = branch.nested_choice.branches[choice]; if (nestedBranch) { await this.executeBranch(nestedBranch); } } } /** * Check prerequisites */ async checkPrerequisites(prerequisites) { for (const prereq of prerequisites) { if (prereq.artifact) { if (!this.workflowState.artifacts[prereq.artifact]) { console.log(chalk.yellow(` āš ļø Missing artifact: ${prereq.artifact}`)); return false; } } if (prereq.decision) { if (!this.workflowState.decisions[prereq.decision]) { console.log(chalk.yellow(` āš ļø Missing decision: ${prereq.decision}`)); return false; } } } return true; } /** * Resolve prerequisites interactively */ async resolvePrerequisites(prerequisites) { console.log(chalk.cyan('\nšŸ”§ Resolving prerequisites...')); for (const prereq of prerequisites) { if (prereq.artifact && !this.workflowState.artifacts[prereq.artifact]) { const { action } = await inquirer.prompt([ { type: 'list', name: 'action', message: `Missing artifact: ${prereq.artifact}. How would you like to proceed?`, choices: [ { name: 'Create it now', value: 'create' }, { name: 'Mark as existing', value: 'mark' }, { name: 'Skip', value: 'skip' }, ], }, ]); if (action === 'create' || action === 'mark') { this.workflowState.artifacts[prereq.artifact] = { resolved: true, resolution: action, timestamp: new Date().toISOString(), }; } } } } /** * Evaluate condition */ async evaluateCondition(condition) { if (condition.context_check) { const contextValue = this.getContextValue(condition.context_check.path); return this.compareValues( contextValue, condition.context_check.value, condition.context_check.operator ); } if (condition.artifact_exists) { return !!this.workflowState.artifacts[condition.artifact_exists]; } if (condition.decision_equals) { const decision = this.workflowState.decisions[condition.decision_equals.decision]; return decision === condition.decision_equals.value; } if (condition.user_confirm) { const { confirmed } = await inquirer.prompt([ { type: 'confirm', name: 'confirmed', message: condition.user_confirm, default: false, }, ]); return confirmed; } return false; } /** * Execute validation gate */ async executeValidationGate(phase) { console.log(chalk.cyan(`\n🚦 Validation Gate: ${phase.validation_gate.name}`)); const validations = phase.validation_gate.checks || []; const results = []; for (const check of validations) { const result = await this.performValidation(check); results.push(result); if (result.passed) { console.log(chalk.green(` āœ“ ${check.name}: PASSED`)); } else { console.log(chalk.red(` āœ— ${check.name}: FAILED`)); } } const allPassed = results.every((r) => r.passed); if (!allPassed && phase.validation_gate.required) { const { action } = await inquirer.prompt([ { type: 'list', name: 'action', message: 'Validation gate failed. How would you like to proceed?', choices: [ { name: 'Fix issues and retry', value: 'retry' }, { name: 'Continue with warnings', value: 'continue' }, { name: 'Abort workflow', value: 'abort' }, ], }, ]); if (action === 'abort') { throw new Error('Workflow aborted at validation gate'); } else if (action === 'retry') { await this.executeValidationGate(phase); } } this.workflowState.validationResults[phase.name] = results; } /** * Perform individual validation */ async performValidation(check) { // Simulate validation logic if (check.type === 'artifact_exists') { return { name: check.name, passed: !!this.workflowState.artifacts[check.artifact], }; } if (check.type === 'manual_review') { const { passed } = await inquirer.prompt([ { type: 'confirm', name: 'passed', message: `Manual validation: ${check.question}`, default: false, }, ]); return { name: check.name, passed }; } if (check.type === 'automated') { // Simulate automated check return { name: check.name, passed: Math.random() > 0.3, // 70% pass rate for demo }; } return { name: check.name, passed: true }; } /** * Get context value by path */ getContextValue(path) { const parts = path.split('.'); let value = this.workflowState.context; for (const part of parts) { value = value[part]; if (value === undefined) break; } return value; } /** * Compare values with operator */ compareValues(value1, value2, operator = '==') { switch (operator) { case '==': case 'equals': return value1 === value2; case '!=': case 'not_equals': return value1 !== value2; case 'includes': return Array.isArray(value1) ? value1.includes(value2) : false; case 'in': return Array.isArray(value2) ? value2.includes(value1) : false; default: return false; } } /** * Save workflow session */ async saveSession() { const sessionId = `session-${Date.now()}`; const sessionPath = path.join(this.options.outputDir, `${sessionId}.yaml`); await fs.ensureDir(this.options.outputDir); const session = { id: sessionId, workflow: this.currentWorkflow.workflow.id, timestamp: new Date().toISOString(), state: this.workflowState, choices: this.choiceHistory, branch_path: this.branchPath, }; await fs.writeFile(sessionPath, yaml.dump(session), 'utf-8'); console.log(chalk.green(`\nšŸ’¾ Session saved: ${sessionPath}`)); } } // CLI Interface if (require.main === module) { const program = require('commander'); program .version('1.0.0') .description('Interactive Workflow Builder for Salesforce projects') .argument('[workflow]', 'Workflow ID to execute') .option('-d, --dir <directory>', 'Workflow directory', 'sf-core/workflows') .option('-o, --output <directory>', 'Output directory', 'docs/workflow-sessions') .option('-v, --verbose', 'Verbose output') .action(async (workflow, options) => { try { const builder = new InteractiveWorkflowBuilder({ workflowDir: options.dir, outputDir: options.output, verbose: options.verbose, }); if (!workflow) { // List available workflows const files = await fs.readdir(options.dir); const workflows = files.filter((f) => f.endsWith('.yaml')); console.log(chalk.blue('\nšŸ“š Available workflows:')); workflows.forEach((w) => { console.log(` • ${w.replace('.yaml', '')}`); }); const { selected } = await inquirer.prompt([ { type: 'list', name: 'selected', message: 'Select a workflow to execute:', choices: workflows.map((w) => w.replace('.yaml', '')), }, ]); workflow = selected; } await builder.startWorkflow(workflow); } catch (error) { console.error(chalk.red(`\nāŒ Error: ${error.message}`)); process.exit(1); } }); program.parse(); } module.exports = InteractiveWorkflowBuilder;