/** * Workflow Graph Engine (IR Layer) — UPGRADED * Builds an internal graph representation BEFORE compiling to n8n JSON * Provider-agnostic LLM Gateway — NO direct OpenAI dependency * Validates all node types against the real Node Registry before returning */ import { type LLMGateway } from '@wfo/integrations/llm-providers/index'; import type { WorkflowIntent, WorkflowArchitecturePlan, WorkflowGraph } from '../types/workflow'; import { GRAPH_ENGINE_PROMPT } from '../prompts/graphEngine'; import { isValidNodeType, getRegistryNodeList } from '../knowledge/nodeRegistry'; export class WorkflowGraphEngine { private llm: LLMGateway; constructor(llm: LLMGateway) { this.llm = llm; } async buildGraph( userRequest: string, intent: WorkflowIntent, plan: WorkflowArchitecturePlan, ): Promise { const registryList = getRegistryNodeList(); const graph = await this.llm.completeJSON([ { role: 'system', content: GRAPH_ENGINE_PROMPT }, { role: 'user', content: `Build a WorkflowGraph IR for this workflow: REQUEST: ${userRequest} INTENT: ${JSON.stringify(intent, null, 2)} ARCHITECTURE PLAN: ${JSON.stringify(plan, null, 2)} AVAILABLE NODE TYPES (USE ONLY THESE — NO OTHERS): ${registryList} CRITICAL RULES: - Every node MUST have n8nNodeType from the list above ONLY - NEVER invent node types not in the list - Every non-trigger node MUST have at least one incoming edge - Every node MUST have meaningful parameters — NO empty nodes - DataContracts must define what JSON fields flow between nodes - Use real expressions: {{$json?.field ?? ""}} — NOT placeholder text - Position nodes in clean left-to-right layout (x increases by 220 per step) - Return a complete WorkflowGraph JSON`, }, ], { temperature: 0.0, retries: 3, }); return this.validateAndOptimizeGraph(graph); } /** * Validates all node types against registry, removes unknown nodes, * optimises graph structure (orphan removal, position cleanup) */ private validateAndOptimizeGraph(graph: WorkflowGraph): WorkflowGraph { const unknownNodes: string[] = []; // Filter out any hallucinated node types const validNodes = graph.nodes.filter((node) => { if (!isValidNodeType(node.n8nNodeType)) { unknownNodes.push(`${node.label} (${node.n8nNodeType})`); return false; } return true; }); if (unknownNodes.length > 0) { console.warn( `[GraphEngine] REJECTED ${unknownNodes.length} unknown node type(s): ${unknownNodes.join(', ')}`, ); } const validNodeIds = new Set(validNodes.map((n) => n.id)); // Remove edges that reference removed nodes const validEdges = graph.edges.filter( (e) => validNodeIds.has(e.sourceNodeId) && validNodeIds.has(e.targetNodeId), ); // Remove orphaned non-trigger nodes const optimizedNodes = validNodes.filter((node) => { const hasIncoming = validEdges.some((e) => e.targetNodeId === node.id); const hasOutgoing = validEdges.some((e) => e.sourceNodeId === node.id); const isTrigger = node.layer === 'trigger'; return isTrigger || hasIncoming || hasOutgoing; }); return { ...graph, nodes: optimizedNodes, edges: validEdges, metadata: { ...graph.metadata, version: '2.0.0', createdAt: new Date().toISOString(), unknownNodesRejected: unknownNodes, }, }; } }