{
  "id": "ft-trigger-09",
  "meta": {
    "instanceId": "vorlux-hub"
  },
  "name": "Vorlux AI | Fine-Tune Trigger (Manual)",
  "active": true,
  "nodes": [
    {
      "id": "c9d0e1f2-0009-4ccc-8009-000000000001",
      "name": "Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [220, 300],
      "parameters": {
        "path": "finetune-trigger",
        "httpMethod": "POST",
        "responseMode": "onReceived",
        "options": {}
      }
    },
    {
      "id": "c9d0e1f2-0009-4ccc-8009-000000000002",
      "name": "Check Dataset Quality",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [460, 300],
      "parameters": {
        "method": "GET",
        "url": "={{$env.VORLUX_HUB_URL}}/api/admin/finetune/quality-report?latest=true",
        "options": {
          "timeout": 15000
        }
      },
      "notes": "Gets the latest quality report to verify dataset is ready"
    },
    {
      "id": "c9d0e1f2-0009-4ccc-8009-000000000003",
      "name": "Quality Gate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [700, 300],
      "notes": "Checks if dataset meets minimum quality thresholds for training",
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const report = $input.first().json.data || {};\nconst webhookParams = $('Webhook Trigger').first().json.body || {};\n\nconst qualityScore = report.qualityScore || 0;\nconst totalExamples = report.totalExamples || 0;\nconst force = webhookParams.force === true;\n\nconst minQuality = 80;\nconst minExamples = 200;\n\nconst canTrain = force || (qualityScore >= minQuality && totalExamples >= minExamples);\nconst reason = !canTrain ? `Quality: ${qualityScore}% (min ${minQuality}%), Examples: ${totalExamples} (min ${minExamples})` : 'OK';\n\nconst baseModel = webhookParams.baseModel || 'qwen2.5:7b';\nconst modelName = webhookParams.modelName || 'workflow-agent-v2';\n\nreturn [{ json: { canTrain, reason, force, qualityScore, totalExamples, baseModel, modelName, config: { loraRank: 16, loraAlpha: 32, epochs: webhookParams.epochs || 3, learningRate: webhookParams.learningRate || 2e-5 } } }];"
      }
    },
    {
      "id": "c9d0e1f2-0009-4ccc-8009-000000000004",
      "name": "Can Train?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [940, 300],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.canTrain }}",
              "rightValue": "true",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "c9d0e1f2-0009-4ccc-8009-000000000005",
      "name": "Create Ollama Model",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1200, 200],
      "notes": "Creates/updates the Ollama model from the Modelfile with training data",
      "parameters": {
        "method": "POST",
        "url": "={{$env.VORLUX_HUB_URL}}/api/admin/finetune/train",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ baseModel: $json.baseModel, modelName: $json.modelName, config: $json.config, trainingFile: 'combined-finetune-v2.jsonl' }) }}",
        "options": {
          "timeout": 300000
        }
      }
    },
    {
      "id": "c9d0e1f2-0009-4ccc-8009-000000000006",
      "name": "Test New Model",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1440, 200],
      "notes": "Tests the new model with sample prompts and validates outputs",
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const ollamaUrl = $env.OLLAMA_BASE_URL || 'http://localhost:11434';\nconst modelName = $input.first().json.data?.modelName || 'workflow-agent-v2';\n\nconst testPrompts = [\n  'Create an n8n workflow that monitors RSS feeds and posts to Discord',\n  'Build a workflow that pulls YouTube transcripts and creates blog posts',\n  'Design a workflow for daily health checks of all services'\n];\n\nconst results = [];\nfor (const prompt of testPrompts) {\n  try {\n    const res = await fetch(ollamaUrl + '/api/generate', {\n      method: 'POST',\n      headers: {'Content-Type':'application/json'},\n      body: JSON.stringify({ model: modelName, prompt, stream: false, options: { temperature: 0.3 } }),\n      signal: AbortSignal.timeout(60000)\n    });\n    const data = await res.json();\n    const output = data.response || '';\n    \n    // Validate output\n    let isValidJson = false;\n    let hasNodes = false;\n    let hasConnections = false;\n    try {\n      const parsed = JSON.parse(output);\n      isValidJson = true;\n      hasNodes = Array.isArray(parsed.nodes) && parsed.nodes.length > 0;\n      hasConnections = typeof parsed.connections === 'object';\n    } catch {}\n    \n    results.push({\n      prompt: prompt.substring(0, 80),\n      outputLength: output.length,\n      isValidJson,\n      hasNodes,\n      hasConnections,\n      score: (isValidJson ? 40 : 0) + (hasNodes ? 30 : 0) + (hasConnections ? 30 : 0)\n    });\n  } catch (err) {\n    results.push({ prompt: prompt.substring(0, 80), error: String(err).substring(0, 100), score: 0 });\n  }\n}\n\nconst avgScore = results.reduce((s, r) => s + r.score, 0) / results.length;\nreturn [{ json: { results, avgScore, modelName, passed: avgScore >= 60 } }];"
      }
    },
    {
      "id": "c9d0e1f2-0009-4ccc-8009-000000000007",
      "name": "Discord Success",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1700, 200],
      "parameters": {
        "method": "POST",
        "url": "={{$env.DISCORD_OPS_WEBHOOK}}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\"embeds\":[{\"title\":\"Fine-Tuning Complete\",\"description\":\"**Model:** {{ $json.modelName }}\\n**Test Score:** {{ $json.avgScore }}%\\n**Status:** {{ $json.passed ? 'PASSED' : 'NEEDS REVIEW' }}\\n\\n**Test Results:**\\n{{ $json.results.map(r => (r.score >= 60 ? '\\u2705' : '\\u274c') + ' ' + r.prompt + ' (score: ' + r.score + ')').join('\\\\n') }}\",\"color\":{{ $json.passed ? 5763719 : 15548997 }},\"footer\":{\"text\":\"Fine-Tune Trigger\"}}]}",
        "options": {
          "timeout": 10000
        }
      }
    },
    {
      "id": "c9d0e1f2-0009-4ccc-8009-000000000008",
      "name": "Discord Blocked",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1200, 450],
      "parameters": {
        "method": "POST",
        "url": "={{$env.DISCORD_OPS_WEBHOOK}}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\"embeds\":[{\"title\":\"Fine-Tuning Blocked\",\"description\":\"Cannot train: {{ $json.reason }}\\nQuality: {{ $json.qualityScore }}%\\nExamples: {{ $json.totalExamples }}\\n\\nUse force:true to override.\",\"color\":15548997,\"footer\":{\"text\":\"Fine-Tune Trigger\"}}]}",
        "options": {
          "timeout": 10000
        }
      }
    }
  ],
  "connections": {
    "Webhook Trigger": {
      "main": [
        [
          {
            "node": "Check Dataset Quality",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Dataset Quality": {
      "main": [
        [
          {
            "node": "Quality Gate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Quality Gate": {
      "main": [
        [
          {
            "node": "Can Train?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Can Train?": {
      "main": [
        [
          {
            "node": "Create Ollama Model",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Discord Blocked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Ollama Model": {
      "main": [
        [
          {
            "node": "Test New Model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Test New Model": {
      "main": [
        [
          {
            "node": "Discord Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": true,
    "saveExecutionProgress": true
  },
  "tags": [
    { "name": "ai" },
    { "name": "finetune" },
    { "name": "training" },
    { "name": "manual" }
  ],
  "versionId": "2"
}