File size: 3,889 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
/**
 * Workflow Compiler Agent β€” UPGRADED
 * Converts WorkflowGraph (IR) β†’ valid n8n JSON
 * Schema-aware, version-aware, credential-aware
 * Provider-agnostic LLM Gateway β€” NO direct OpenAI dependency
 * STRICT: Only uses nodes from registry. Zero hallucinated params.
 */
import { type LLMGateway } from '@wfo/integrations/llm-providers/index';
import type { WorkflowGraph, WorkflowIntent, N8nWorkflow } from '../types/workflow';
import { COMPILER_PROMPT } from '../prompts/compiler';
import { N8N_NODE_REGISTRY, isValidNodeType } from '../knowledge/nodeRegistry';

export class WorkflowCompiler {
  private llm: LLMGateway;

  constructor(llm: LLMGateway) {
    this.llm = llm;
  }

  async compile(graph: WorkflowGraph, intent: WorkflowIntent): Promise<N8nWorkflow> {
    // Build node schema context from registry (ONLY nodes used in graph)
    const nodeTypes = [...new Set(graph.nodes.map((n) => n.n8nNodeType))];
    const validNodeTypes = nodeTypes.filter(isValidNodeType);
    const nodeSchemas = validNodeTypes
      .map((type) => N8N_NODE_REGISTRY[type])
      .filter(Boolean)
      .map((s) => JSON.stringify(s, null, 2))
      .join('\n\n');

    const workflow = await this.llm.completeJSON<N8nWorkflow>([
      { role: 'system', content: COMPILER_PROMPT },
      {
        role: 'user',
        content: `Compile this WorkflowGraph to a valid n8n JSON workflow.

GRAPH:
${JSON.stringify(graph, null, 2)}

INTENT:
${JSON.stringify(intent, null, 2)}

REGISTERED NODE SCHEMAS (use ONLY these β€” parameters MUST match schema):
${nodeSchemas || '(No pre-built schemas β€” use standard n8n defaults)'}

CRITICAL COMPILATION RULES:
1. active MUST be false β€” NEVER true
2. All node IDs must be unique UUID strings (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
3. connections MUST wire ALL graph edges correctly
4. NEVER invent parameters not in the node schema above
5. Every node MUST have real expressions for dynamic fields β€” NO static placeholder text
6. Add retryOnFail: true, maxTries: 3, waitBetweenTries: 1000 on all critical/external nodes
7. Add meaningful node notes explaining what each node does
8. USE these expression patterns:
   - Input from trigger: {{$json?.body?.fieldName ?? $json?.fieldName ?? ""}}
   - Previous node: {{$node["NodeName"].json?.field ?? ""}}
   - Array item: {{$json?.items?.[0]?.value ?? ""}}
   - Conditional: {{$json?.status === "active" ? "yes" : "no"}}
9. SET nodes MUST have explicit field mappings β€” NOT empty values array
10. IF nodes MUST have real boolean conditions referencing actual fields
11. CODE nodes MUST contain real JavaScript logic β€” NOT placeholder comments
12. AI Agent nodes MUST include system message AND dynamic user message from previous node data
13. Return ONLY complete, importable n8n JSON workflow`,
      },
    ], {
      temperature: 0.0,
      retries: 3,
    });

    return this.ensureSafeDefaults(workflow);
  }

  /**
   * Enforce safety defaults regardless of LLM output
   * CRITICAL: active = false, proper settings, error handling
   */
  private ensureSafeDefaults(workflow: N8nWorkflow): N8nWorkflow {
    return {
      ...workflow,
      active: false, // NEVER activate on compile β€” ABSOLUTE safety rule
      settings: {
        callerPolicy: 'workflowsFromSameOwner',
        errorWorkflow: workflow.settings?.errorWorkflow,
        timezone: workflow.settings?.timezone ?? 'UTC',
        ...workflow.settings,
        // Force these even if LLM overrides:
        executionOrder: 'v1',
        saveManualExecutions: true,
      },
      nodes: workflow.nodes.map((node) => ({
        ...node,
        onError: node.onError ?? 'continueErrorOutput',
        // Ensure critical external nodes have retry
        retryOnFail: node.retryOnFail ?? false,
        maxTries: node.maxTries ?? 3,
        waitBetweenTries: node.waitBetweenTries ?? 1000,
      })),
    };
  }
}