{ "name": "78 FAQ import + release fixed rows (reference)", "nodes": [ { "name": "Import webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "parameters": { "httpMethod": "POST", "path": "my-faq-import", "responseMode": "responseNode", "options": {} }, "position": [ 0, 300 ], "id": "d3a86b18-edfc-51e7-95c7-8aa2c58fdbda", "webhookId": "c4169baa-ac65-5afd-ac30-52094389dbf7" }, { "name": "Run check", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "parameters": { "url": "http://localhost:5678/webhook/my-faq-check", "options": { "response": { "response": { "neverError": true } }, "timeout": 30000 }, "method": "POST", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify($json.body) }}" }, "position": [ 220, 300 ], "id": "1d6a0bcd-c4ae-5b95-b04a-8c021adbd523" }, { "name": "Checked?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "conditions": [ { "id": "c1", "leftValue": "={{ Array.isArray($json.rows) && $json.rows.length > 0 }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "position": [ 440, 300 ], "id": "b753a2cd-44be-5af3-8683-b154c2443f0f" }, { "name": "Reject body", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, "parameters": { "respondWith": "json", "responseBody": "={{ JSON.stringify({ error: $json.error ?? 'check failed' }) }}", "options": { "responseCode": 400 } }, "position": [ 660, 760 ], "id": "36f0ca97-2abb-53d3-a0da-4fe3e3d2ae74" }, { "name": "Batch ok?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "conditions": [ { "id": "c1", "leftValue": "={{ $json.invalid / $json.total <= ($('Import webhook').first().json.body.max_invalid_ratio ?? 0.3) }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "position": [ 660, 300 ], "id": "edceb049-33db-5490-bac7-bed1554f414f" }, { "name": "Reject batch", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, "parameters": { "respondWith": "json", "responseBody": "={{ JSON.stringify({ error: 'batch rejected', batch: $('Run check').first().json.batch, invalid_ratio: Math.round($('Run check').first().json.invalid / $('Run check').first().json.total * 10000) / 10000, max_invalid_ratio: ($('Import webhook').first().json.body.max_invalid_ratio ?? 0.3), codes: $('Run check').first().json.codes, llm_calls: 0 }) }}", "options": { "responseCode": 422 } }, "position": [ 1320, 560 ], "id": "256d2a55-b303-51f1-a8a5-3ea934fb7f3b" }, { "name": "Log rejected", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "parameters": { "url": "http://data-api:8800/quality-log", "options": { "response": { "response": { "neverError": true } } }, "method": "POST", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ batch: $json.batch, dataset: 'faq', total: $json.total, valid: $json.valid, quarantined: 0, rejected: true, score: 0, codes: $json.codes }) }}" }, "position": [ 880, 560 ], "id": "7f06265c-c872-5e48-8c72-0ea9beaf1cdc" }, { "name": "Alert rejected", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "parameters": { "url": "http://data-api:8800/alerts", "options": { "response": { "response": { "neverError": true } } }, "method": "POST", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ workflow: 'faq-import', node: 'Batch ok?', severity: 'high', reason: 'batch_rejected', batch: $('Run check').first().json.batch, message: $('Run check').first().json.batch + ' rejected' }) }}" }, "position": [ 1100, 560 ], "id": "4ba91d1f-4b86-552d-b572-08706cd4d170" }, { "name": "Split rows", "type": "n8n-nodes-base.code", "typeVersion": 2, "parameters": { "jsCode": "const c = $('Run check').first().json;\nconst items = $('Import webhook').first().json.body.items;\nreturn c.rows.map((r, i) => ({ json: { ...r, row: items[i], batch: c.batch } }));" }, "position": [ 880, 300 ], "id": "ebce06e1-7140-5292-8687-4ab2b641c047" }, { "name": "Only passed", "type": "n8n-nodes-base.code", "typeVersion": 2, "parameters": { "jsCode": "return $input.all().filter((i) => i.json.ok);" }, "position": [ 1100, 300 ], "id": "b7a235dd-8495-5b00-a635-3dbd3920a1c4" }, { "name": "Judge", "type": "@n8n/n8n-nodes-langchain.agent", "typeVersion": 3.1, "parameters": { "promptType": "define", "text": "=제목: {{ $json.row.title }}\n답: {{ $json.row.answer }}", "options": { "systemMessage": "You check a company IT FAQ entry before it is published. Does the answer actually answer the title's topic? Ignore style. Return only JSON, no code fence: {\"match\": true, \"reason\": \"short reason in Korean\"}", "maxIterations": 1 } }, "onError": "continueRegularOutput", "position": [ 1320, 300 ], "id": "6273964f-6cb4-56ab-885d-2dc3faf7f7ff" }, { "name": "Judge model", "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", "typeVersion": 1.2, "parameters": { "model": { "__rl": true, "mode": "id", "value": "gpt-4.1-mini" }, "options": { "temperature": 0 } }, "credentials": { "openAiApi": { "id": "openAiCourse0001", "name": "OpenAI (.env)" } }, "position": [ 1320, 520 ], "id": "dd634caa-4367-5cdb-aac1-559d0114d7ff" }, { "name": "Decide", "type": "n8n-nodes-base.code", "typeVersion": 2, "parameters": { "jsCode": "const judged = $('Only passed').all().map((item, i) => {\n let v = { match: false, reason: 'judge unreadable' };\n try { v = JSON.parse(String($('Judge').all()[i].json.output ?? '').replace(/^```(json)?\\s*|```$/g, '')); } catch (e) {}\n return { ...item.json, ok: v.match === true, errors: v.match === true ? [] : ['llm_mismatch'], reason: String(v.reason ?? '') };\n});\nlet k = 0;\nreturn $('Split rows').all().map((i) => ({ json: i.json.ok ? judged[k++] : i.json }));" }, "position": [ 1540, 300 ], "id": "af54c0ea-0a6a-569a-8cc5-9e4b831fda3c" }, { "name": "Store row", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "parameters": { "url": "={{ $json.ok ? 'http://data-api:8800/faq/add' : 'http://data-api:8800/quarantine' }}", "options": { "response": { "response": { "neverError": true } } }, "method": "POST", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify($json.ok ? $json.row : { items: [{ batch: $json.batch, row: $json.row, errors: $json.errors }] }) }}" }, "position": [ 1760, 300 ], "id": "b1b7176d-9a05-5f4b-82e7-71b8264ed74c" }, { "name": "Summarize", "type": "n8n-nodes-base.code", "typeVersion": 2, "parameters": { "jsCode": "const c = $('Run check').first().json;\nconst rows = $('Decide').all().map((i) => i.json);\nconst imported = rows.filter((r) => r.ok);\nconst quarantined = rows.filter((r) => !r.ok);\nconst codes = { ...c.codes };\nfor (const r of quarantined) for (const e of r.errors) if (e === 'llm_mismatch') codes[e] = (codes[e] ?? 0) + 1;\nreturn [{ json: { batch: c.batch, total: c.total, imported: imported.length, quarantined: quarantined.length, rejected: false,\n score: Math.round(imported.length / c.total * 10000) / 10000, max_invalid_ratio: ($('Import webhook').first().json.body.max_invalid_ratio ?? 0.3),\n codes, imported_ids: imported.map((r) => r.id), quarantined_ids: quarantined.map((r) => r.id), llm_calls: $('Only passed').all().length } }];" }, "position": [ 1980, 300 ], "id": "9e940251-c204-5813-9457-72980199a956" }, { "name": "Release fixed", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "parameters": { "url": "http://data-api:8800/quarantine/remove", "options": { "response": { "response": { "neverError": true } } }, "method": "POST", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ ids: $json.imported_ids }) }}" }, "position": [ 2200, 300 ], "id": "7239d9dc-daf2-5aea-a13c-be25358337a2" }, { "name": "Log quality", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "parameters": { "url": "http://data-api:8800/quality-log", "options": { "response": { "response": { "neverError": true } } }, "method": "POST", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ batch: $('Summarize').first().json.batch, dataset: 'faq', total: $('Summarize').first().json.total, valid: $('Summarize').first().json.imported, quarantined: $('Summarize').first().json.quarantined, rejected: false, score: $('Summarize').first().json.score, codes: $('Summarize').first().json.codes }) }}" }, "position": [ 2420, 300 ], "id": "67ebf40c-0364-5bde-9f95-eb77a10689c3" }, { "name": "Low score?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "conditions": [ { "id": "c1", "leftValue": "={{ $('Summarize').first().json.score < 0.9 }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "position": [ 2640, 300 ], "id": "4f8eb1e7-753f-5515-8485-e9b0ff7d8bc7" }, { "name": "Alert low score", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "parameters": { "url": "http://data-api:8800/alerts", "options": { "response": { "response": { "neverError": true } } }, "method": "POST", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ workflow: 'faq-import', node: 'Low score?', severity: 'high', reason: 'low_quality', batch: $('Summarize').first().json.batch, score: $('Summarize').first().json.score, message: $('Summarize').first().json.batch + ' quality ' + $('Summarize').first().json.score }) }}" }, "position": [ 2860, 160 ], "id": "68033c5c-b22b-535f-9ddc-ff603ab7d3c2" }, { "name": "Result again", "type": "n8n-nodes-base.code", "typeVersion": 2, "parameters": { "jsCode": "return [{ json: $('Summarize').first().json }];" }, "position": [ 3080, 300 ], "id": "2ca2f7fc-885a-5ac9-9121-62b79d203f74" }, { "name": "Send result", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, "parameters": { "respondWith": "json", "responseBody": "={{ $json }}", "options": {} }, "position": [ 3300, 300 ], "id": "c58470f8-9dd9-521a-be5b-746bbdb86d3a" } ], "connections": { "Import webhook": { "main": [ [ { "node": "Run check", "type": "main", "index": 0 } ] ] }, "Run check": { "main": [ [ { "node": "Checked?", "type": "main", "index": 0 } ] ] }, "Checked?": { "main": [ [ { "node": "Batch ok?", "type": "main", "index": 0 } ], [ { "node": "Reject body", "type": "main", "index": 0 } ] ] }, "Batch ok?": { "main": [ [ { "node": "Split rows", "type": "main", "index": 0 } ], [ { "node": "Log rejected", "type": "main", "index": 0 } ] ] }, "Log rejected": { "main": [ [ { "node": "Alert rejected", "type": "main", "index": 0 } ] ] }, "Alert rejected": { "main": [ [ { "node": "Reject batch", "type": "main", "index": 0 } ] ] }, "Split rows": { "main": [ [ { "node": "Only passed", "type": "main", "index": 0 } ] ] }, "Only passed": { "main": [ [ { "node": "Judge", "type": "main", "index": 0 } ] ] }, "Judge": { "main": [ [ { "node": "Decide", "type": "main", "index": 0 } ] ] }, "Judge model": { "ai_languageModel": [ [ { "node": "Judge", "type": "ai_languageModel", "index": 0 } ] ] }, "Decide": { "main": [ [ { "node": "Store row", "type": "main", "index": 0 } ] ] }, "Store row": { "main": [ [ { "node": "Summarize", "type": "main", "index": 0 } ] ] }, "Summarize": { "main": [ [ { "node": "Release fixed", "type": "main", "index": 0 } ] ] }, "Release fixed": { "main": [ [ { "node": "Log quality", "type": "main", "index": 0 } ] ] }, "Log quality": { "main": [ [ { "node": "Low score?", "type": "main", "index": 0 } ] ] }, "Low score?": { "main": [ [ { "node": "Alert low score", "type": "main", "index": 0 } ], [ { "node": "Result again", "type": "main", "index": 0 } ] ] }, "Alert low score": { "main": [ [ { "node": "Result again", "type": "main", "index": 0 } ] ] }, "Result again": { "main": [ [ { "node": "Send result", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1" } }