File size: 3,511 Bytes
dd480ef | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | /**
* 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<WorkflowGraph> {
const registryList = getRegistryNodeList();
const graph = await this.llm.completeJSON<WorkflowGraph>([
{ 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,
},
};
}
}
|