PYAE1994's picture
Upload folder using huggingface_hub
dd480ef verified
/**
* Self-Healing System β€” NEW
* Detects root cause of workflow failures and applies corrective transformations
* Stores failure patterns in memory, prevents repeat failures
* Integrates with validation + simulation pipeline
*/
import { type LLMGateway } from '@wfo/integrations/llm-providers/index';
import type { ValidationReport, SimulationReport, N8nWorkflow, WorkflowGraph } from '../types/workflow';
import { isValidNodeType } from '../knowledge/nodeRegistry';
export interface HealingAction {
type: 'replace_node' | 'fix_expression' | 'add_parameter' | 'fix_connection' | 'add_retry' | 'remove_node';
targetNodeId?: string;
description: string;
applied: boolean;
before?: unknown;
after?: unknown;
}
export interface HealingResult {
healed: boolean;
actionsApplied: HealingAction[];
remainingIssues: string[];
healedWorkflow?: N8nWorkflow;
confidence: number; // 0-100
}
export interface FailurePattern {
id: string;
pattern: string;
rootCause: string;
fixStrategy: string;
occurrences: number;
lastSeen: string;
}
// In-memory failure store (persists across requests in a single process)
const failureMemory: Map<string, FailurePattern> = new Map();
export class SelfHealingSystem {
private llm: LLMGateway;
constructor(llm: LLMGateway) {
this.llm = llm;
}
/**
* Attempt to auto-heal a workflow based on validation and simulation reports
*/
async heal(
workflow: N8nWorkflow,
graph: WorkflowGraph,
validationReport: ValidationReport,
simulationReport?: SimulationReport,
): Promise<HealingResult> {
const actions: HealingAction[] = [];
let healedWorkflow = structuredClone(workflow);
// ─── Phase 1: Rule-based deterministic healing ────────────────────────
const ruleBasedActions = this.applyRuleBasedFixes(healedWorkflow, validationReport);
actions.push(...ruleBasedActions.actions);
healedWorkflow = ruleBasedActions.workflow;
// ─── Phase 2: AI-assisted healing for complex issues ──────────────────
const complexIssues = validationReport.issues.filter((i) =>
i.severity === 'error' &&
!actions.some((a) => a.targetNodeId === i.nodeId && a.applied),
);
if (complexIssues.length > 0) {
try {
const aiHealing = await this.applyAIHealing(healedWorkflow, graph, complexIssues, simulationReport);
actions.push(...aiHealing.actions);
if (aiHealing.healedWorkflow) {
healedWorkflow = aiHealing.healedWorkflow;
}
} catch (err) {
console.warn('[SelfHealing] AI healing phase failed:', err);
}
}
// ─── Phase 3: Record failure pattern ─────────────────────────────────
this.recordFailurePattern(validationReport, simulationReport, actions);
const appliedCount = actions.filter((a) => a.applied).length;
const remainingErrors = validationReport.issues.filter((i) =>
i.severity === 'error' &&
!actions.some((a) => a.targetNodeId === i.nodeId && a.applied),
);
return {
healed: appliedCount > 0 && remainingErrors.length === 0,
actionsApplied: actions.filter((a) => a.applied),
remainingIssues: remainingErrors.map((i) => i.message),
healedWorkflow: appliedCount > 0 ? healedWorkflow : undefined,
confidence: Math.round((appliedCount / Math.max(1, actions.length)) * 100),
};
}
// ─── Rule-Based Fixes (deterministic, no LLM needed) ─────────────────────
private applyRuleBasedFixes(
workflow: N8nWorkflow,
report: ValidationReport,
): { workflow: N8nWorkflow; actions: HealingAction[] } {
const actions: HealingAction[] = [];
// Fix 1: active must be false
if ((workflow.active as any) === true) {
workflow.active = false;
actions.push({
type: 'fix_expression',
description: 'Set active: false (safety rule β€” never auto-activate)',
applied: true,
before: true,
after: false,
});
}
// Fix 2: Missing node IDs β†’ generate UUID-like IDs
workflow.nodes.forEach((node) => {
if (!node.id || node.id.trim() === '') {
const newId = generateId();
actions.push({
type: 'add_parameter',
targetNodeId: node.name,
description: `Generated missing ID for node "${node.name}"`,
applied: true,
before: node.id,
after: newId,
});
node.id = newId;
}
});
// Fix 3: Missing typeVersion β†’ set to 1
workflow.nodes.forEach((node) => {
if (!node.typeVersion || node.typeVersion < 1) {
actions.push({
type: 'add_parameter',
targetNodeId: node.id,
description: `Added missing typeVersion: 1 to node "${node.name}"`,
applied: true,
before: node.typeVersion,
after: 1,
});
node.typeVersion = 1;
}
});
// Fix 4: Missing position β†’ assign default grid positions
workflow.nodes.forEach((node, i) => {
if (!node.position || !Array.isArray(node.position) || node.position.length !== 2) {
const newPos: [number, number] = [i * 220, 300];
actions.push({
type: 'fix_expression',
targetNodeId: node.id,
description: `Set default position for node "${node.name}"`,
applied: true,
before: node.position,
after: newPos,
});
node.position = newPos;
}
});
// Fix 5: Add retry to external nodes that support it
workflow.nodes.forEach((node) => {
const externalTypes = [
'n8n-nodes-base.httpRequest',
'n8n-nodes-base.telegram',
'n8n-nodes-base.slack',
'n8n-nodes-base.gmail',
'n8n-nodes-base.openAi',
'@n8n/n8n-nodes-langchain.agent',
'n8n-nodes-base.googleSheets',
'n8n-nodes-base.airtable',
'n8n-nodes-base.notion',
'n8n-nodes-base.postgres',
'n8n-nodes-base.hubspot',
'n8n-nodes-base.stripe',
'n8n-nodes-base.github',
];
if (externalTypes.includes(node.type) && !node.retryOnFail) {
actions.push({
type: 'add_retry',
targetNodeId: node.id,
description: `Added retry policy to external node "${node.name}"`,
applied: true,
before: { retryOnFail: false },
after: { retryOnFail: true, maxTries: 3, waitBetweenTries: 1000 },
});
node.retryOnFail = true;
node.maxTries = node.maxTries ?? 3;
node.waitBetweenTries = node.waitBetweenTries ?? 1000;
}
});
// Fix 6: onError field default
workflow.nodes.forEach((node) => {
if (!node.onError) {
node.onError = 'continueErrorOutput';
actions.push({
type: 'add_parameter',
targetNodeId: node.id,
description: `Set onError: continueErrorOutput for node "${node.name}"`,
applied: true,
before: undefined,
after: 'continueErrorOutput',
});
}
});
// Fix 7: Remove nodes with unknown types from registry
const unknownNodes = workflow.nodes.filter((n) => n.type && !isValidNodeType(n.type));
unknownNodes.forEach((node) => {
actions.push({
type: 'remove_node',
targetNodeId: node.id,
description: `REJECTED unknown node type "${node.type}" for node "${node.name}" β€” not in registry`,
applied: true,
before: node.type,
after: null,
});
});
if (unknownNodes.length > 0) {
const unknownIds = new Set(unknownNodes.map((n) => n.id));
workflow.nodes = workflow.nodes.filter((n) => !unknownIds.has(n.id));
}
// Fix 8: Ensure settings are complete
if (!workflow.settings) {
workflow.settings = {};
actions.push({
type: 'add_parameter',
description: 'Added missing workflow settings object',
applied: true,
before: undefined,
after: {},
});
}
if (!workflow.settings.executionOrder) {
workflow.settings.executionOrder = 'v1';
actions.push({
type: 'add_parameter',
description: 'Set settings.executionOrder: v1',
applied: true,
});
}
return { workflow, actions };
}
// ─── AI-Assisted Healing (for complex expression / logic issues) ──────────
private async applyAIHealing(
workflow: N8nWorkflow,
graph: WorkflowGraph,
issues: Array<{ message: string; nodeId?: string; suggestion: string; category: string }>,
simulationReport?: SimulationReport,
): Promise<{ actions: HealingAction[]; healedWorkflow?: N8nWorkflow }> {
// Find the historical fix if available
const historicalFixes = this.getRelevantHistoricalFixes(issues.map((i) => i.message));
const result = await this.llm.completeJSON<{
fixedWorkflow: N8nWorkflow;
actionsApplied: Array<{ nodeId?: string; description: string; type: string }>;
}>([
{
role: 'system',
content: `You are an n8n workflow self-healing system. Your job is to fix broken workflows.
Fix ONLY the identified issues. Do not change anything else.
NEVER set active: true.
ALWAYS use real expressions like {{$json?.field ?? ""}}.
Return the complete fixed workflow JSON.`,
},
{
role: 'user',
content: `Fix these workflow issues:
CURRENT WORKFLOW:
${JSON.stringify(workflow, null, 2)}
ISSUES TO FIX:
${issues.map((i, idx) => `${idx + 1}. [${i.category}] ${i.message}\n Fix: ${i.suggestion}`).join('\n')}
${simulationReport ? `SIMULATION FAILURES:\n${JSON.stringify(simulationReport.predictedFailurePoints, null, 2)}` : ''}
${historicalFixes.length > 0 ? `HISTORICAL FIX STRATEGIES:\n${historicalFixes.join('\n')}` : ''}
Return JSON:
{
"fixedWorkflow": { ... complete fixed n8n workflow ... },
"actionsApplied": [{ "nodeId": "...", "description": "what was fixed", "type": "fix_type" }]
}`,
},
], {
temperature: 0.0,
retries: 2,
});
const actions: HealingAction[] = (result.actionsApplied ?? []).map((a) => ({
type: a.type as HealingAction['type'],
targetNodeId: a.nodeId,
description: a.description,
applied: true,
}));
return {
actions,
healedWorkflow: result.fixedWorkflow ? {
...result.fixedWorkflow,
active: false, // Always enforce safety
} : undefined,
};
}
// ─── Failure Pattern Memory ───────────────────────────────────────────────
private recordFailurePattern(
validation: ValidationReport,
simulation: SimulationReport | undefined,
actions: HealingAction[],
): void {
validation.issues.forEach((issue) => {
const key = `${issue.category}-${issue.id}`;
const existing = failureMemory.get(key);
const action = actions.find((a) => a.targetNodeId === issue.nodeId);
if (existing) {
failureMemory.set(key, {
...existing,
occurrences: existing.occurrences + 1,
lastSeen: new Date().toISOString(),
fixStrategy: action?.description ?? existing.fixStrategy,
});
} else {
failureMemory.set(key, {
id: key,
pattern: issue.message,
rootCause: issue.category,
fixStrategy: action?.description ?? issue.suggestion,
occurrences: 1,
lastSeen: new Date().toISOString(),
});
}
});
}
private getRelevantHistoricalFixes(issueMessages: string[]): string[] {
const fixes: string[] = [];
failureMemory.forEach((pattern) => {
const isRelevant = issueMessages.some(
(msg) => msg.toLowerCase().includes(pattern.rootCause.toLowerCase()),
);
if (isRelevant && pattern.occurrences > 1) {
fixes.push(`Pattern: "${pattern.pattern}" β†’ Fix: "${pattern.fixStrategy}" (seen ${pattern.occurrences}x)`);
}
});
return fixes.slice(0, 5); // Top 5 most relevant
}
/**
* Get current failure memory stats (for reporting)
*/
getMemoryStats(): { totalPatterns: number; topFailures: FailurePattern[] } {
const patterns = [...failureMemory.values()];
const sorted = patterns.sort((a, b) => b.occurrences - a.occurrences);
return {
totalPatterns: patterns.length,
topFailures: sorted.slice(0, 10),
};
}
}
function generateId(): string {
const hex = () => Math.floor(Math.random() * 0x10000).toString(16).padStart(4, '0');
return `${hex()}${hex()}-${hex()}-${hex()}-${hex()}-${hex()}${hex()}${hex()}`;
}