| |
| |
| |
| |
| |
| |
| 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); |
| } |
|
|
| |
| |
| |
| |
| private validateAndOptimizeGraph(graph: WorkflowGraph): WorkflowGraph { |
| const unknownNodes: string[] = []; |
|
|
| |
| 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)); |
|
|
| |
| const validEdges = graph.edges.filter( |
| (e) => validNodeIds.has(e.sourceNodeId) && validNodeIds.has(e.targetNodeId), |
| ); |
|
|
| |
| 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, |
| }, |
| }; |
| } |
| } |
|
|