78일차 데이터 품질 검증 자동화: 실습 15개(Q) · 기준 둘 · 강의(개요 30장 · 단계 문서 · 4단계 정리 · 05 바이브) · 요청서와 VIBE-Q(tcheck), 정답은 수업 뒤
This commit is contained in:
1 parent
fbed1b22f6
commit
4ec7ae123b
76 files changed
+11631
No files matched your search
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"name": "78 FAQ check (team baseline)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Check webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "base-faq-check",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
],
|
||||
"id": "154bbb29-0cd9-55c8-a318-872a5690cb25",
|
||||
"webhookId": "879f1ed6-94a8-57f2-9cc3-8dfa0880a80a"
|
||||
},
|
||||
{
|
||||
"name": "Check rows",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const body = $('Check webhook').first().json.body;\n// team baseline: only empty values (written in a hurry last month)\nconst rows = body.items.map((r) => {\n const errors = [];\n if (!r.title) errors.push('title_missing');\n if (!r.answer) errors.push('answer_missing');\n if (!r.keywords || r.keywords.length === 0) errors.push('keywords_missing');\n return { id: String(r.id ?? ''), ok: errors.length === 0, errors, warnings: [] };\n});\nconst codes = {};\nfor (const row of rows) for (const e of row.errors) codes[e] = (codes[e] ?? 0) + 1;\nconst valid = rows.filter((r) => r.ok).length;\nreturn [{ json: { batch: body.batch ?? '', total: rows.length, valid, invalid: rows.length - valid, rows, codes } }];"
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"id": "072e3548-3d37-52bc-816a-4f5e6c8e795b"
|
||||
},
|
||||
{
|
||||
"name": "Send result",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"id": "4707a40b-6783-5666-bb73-227a848955ac"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Check webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Check rows",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check rows": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"name": "78 FAQ import (team baseline)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Import webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "base-faq-import",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
],
|
||||
"id": "43f87175-3a0a-573c-b690-db9ee8f98200",
|
||||
"webhookId": "14d69cee-d6ad-5163-b231-403811afe39d"
|
||||
},
|
||||
{
|
||||
"name": "Split rows",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const body = $('Import webhook').first().json.body;\n// team baseline: put every row in, the data API will complain if something is really wrong\nreturn body.items.map((row) => ({ json: { row, batch: body.batch ?? '' } }));"
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"id": "a9733297-069f-5009-8455-7dbe7ca59923"
|
||||
},
|
||||
{
|
||||
"name": "Add FAQ",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/faq/add",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify($json.row) }}"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"id": "f2026680-4cfd-551d-bc90-827b8a136c3f"
|
||||
},
|
||||
{
|
||||
"name": "Summarize",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const rows = $('Split rows').all().map((i) => i.json);\nconst answers = $('Add FAQ').all().map((i) => i.json);\nconst imported = rows.filter((r, i) => answers[i]?.added !== undefined);\nreturn [{ json: { batch: rows[0]?.batch ?? '', total: rows.length, imported: imported.length, quarantined: 0, rejected: false,\n score: rows.length ? Math.round(imported.length / rows.length * 10000) / 10000 : 0,\n imported_ids: imported.map((r) => String(r.row.id)), quarantined_ids: [] } }];"
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
],
|
||||
"id": "e758bb72-6ab3-577f-b05f-46e5c117d634"
|
||||
},
|
||||
{
|
||||
"name": "Send result",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
300
|
||||
],
|
||||
"id": "42bdf7fe-b73c-5c8c-b599-277a48f829b0"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Import webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Split rows",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Split rows": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Add FAQ",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Add FAQ": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Summarize",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Summarize": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"name": "78 FAQ check (reference)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Check webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-faq-check",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
],
|
||||
"id": "7f191b41-c9cb-53ba-8811-77f4c563ae66",
|
||||
"webhookId": "32a0de1e-2479-5dbe-8d14-3d95f1e3cb9c"
|
||||
},
|
||||
{
|
||||
"name": "Is a list?",
|
||||
"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.body.items) }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"id": "a43d0f21-b799-5552-a9e0-34296447da2f"
|
||||
},
|
||||
{
|
||||
"name": "Reject body",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "{ \"error\": \"items must be a list\" }",
|
||||
"options": {
|
||||
"responseCode": 400
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
560
|
||||
],
|
||||
"id": "7baa4654-ee8f-597e-887f-ff49602a8280"
|
||||
},
|
||||
{
|
||||
"name": "Check rows",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const body = $('Check webhook').first().json.body;\nconst seen = new Set();\nconst rows = body.items.map((r) => {\n const errors = [], warnings = [];\n const id = String(r?.id ?? '');\n const title = String(r?.title ?? '').trim();\n const answer = String(r?.answer ?? '').trim();\n if (!/^FAQ-\\d{3}$/.test(id)) errors.push('id_format');\n if (!title) errors.push('title_missing'); else if (title.length > 40) errors.push('title_too_long');\n if (!Array.isArray(r?.keywords) || r.keywords.length === 0) errors.push('keywords_missing');\n if (!answer) errors.push('answer_missing'); else if (answer.length < 10) errors.push('answer_too_short'); else if (answer.length > 300) errors.push('answer_too_long');\n if (seen.has(id)) errors.push('duplicate_in_batch');\n seen.add(id);\n if (/01[016789][-. ]?\\d{3,4}[-. ]?\\d{4}/.test(answer) || /[\\w.+-]+@[\\w-]+\\.[\\w.]+/.test(answer)) errors.push('pii');\n if (/https?:\\/\\/(?!intra\\.example\\.com(?![\\w.-]))/i.test(answer)) errors.push('external_link');\n return { id, ok: errors.length === 0, errors, warnings };\n});\nconst codes = {};\nfor (const row of rows) for (const e of row.errors) codes[e] = (codes[e] ?? 0) + 1;\nconst valid = rows.filter((r) => r.ok).length;\nconst out = { batch: body.batch ?? '', total: rows.length, valid, invalid: rows.length - valid,\n with_warnings: rows.filter((r) => r.warnings.length).length, rows, codes };\nreturn [{ json: out }];"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"id": "143f98e1-71b7-5da7-b547-7fd5f9516f92"
|
||||
},
|
||||
{
|
||||
"name": "Send result",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
],
|
||||
"id": "c47d329d-d69b-562e-8383-2538c1c9adf1"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Is a list?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Check rows",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Reject body",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check rows": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Is a list?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
{
|
||||
"name": "78 FAQ import + gate (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": "a56860cf-55b7-5565-b18d-ca3e5e9abe79",
|
||||
"webhookId": "f3d72461-ee99-519a-a141-33709a703108"
|
||||
},
|
||||
{
|
||||
"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": "e9b3fe87-c7c3-5eaf-897f-45406aefb704"
|
||||
},
|
||||
{
|
||||
"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": "78c41fec-cc62-5ce7-9c47-ae10eedd7245"
|
||||
},
|
||||
{
|
||||
"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": "57d70716-121e-589f-9617-31756fb52264"
|
||||
},
|
||||
{
|
||||
"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": "a60e6dbb-88a3-59b1-9a97-6efe355e18f1"
|
||||
},
|
||||
{
|
||||
"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 }) }}",
|
||||
"options": {
|
||||
"responseCode": 422
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
560
|
||||
],
|
||||
"id": "a0faba36-ffdc-5d7c-a3ad-27655254174b"
|
||||
},
|
||||
{
|
||||
"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": "666c96f6-be31-54d9-a0dc-924fb089b786"
|
||||
},
|
||||
{
|
||||
"name": "Decide",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "return $input.all();"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
300
|
||||
],
|
||||
"id": "8e4f7788-d9d4-513c-8702-1a4d0218faa5"
|
||||
},
|
||||
{
|
||||
"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": [
|
||||
1320,
|
||||
300
|
||||
],
|
||||
"id": "e35c6cb5-7cc0-5d07-8d38-68d6f7a116d4"
|
||||
},
|
||||
{
|
||||
"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) } }];"
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
300
|
||||
],
|
||||
"id": "80a8e374-c3d2-5f31-bce1-160bd265176c"
|
||||
},
|
||||
{
|
||||
"name": "Send result",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
300
|
||||
],
|
||||
"id": "861cf41c-8519-549b-ba20-7cd0943caf23"
|
||||
}
|
||||
],
|
||||
"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": "Reject batch",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Split rows": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Decide",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Decide": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Store row",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Store row": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Summarize",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Summarize": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
{
|
||||
"name": "78 FAQ import + LLM check (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": "0c06837b-c353-5d57-87a3-9902cc7cb89b",
|
||||
"webhookId": "5df7e654-f73a-5bd7-a684-9c0125bb37a0"
|
||||
},
|
||||
{
|
||||
"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": "113c3307-ae38-53f2-b868-6836377ce3de"
|
||||
},
|
||||
{
|
||||
"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": "5cebd576-bdd0-5d87-8d21-50dedd8f7330"
|
||||
},
|
||||
{
|
||||
"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": "28279a25-ff11-545e-8665-853b391d4cc2"
|
||||
},
|
||||
{
|
||||
"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": "79dafedb-7560-5604-92df-e74301c2e851"
|
||||
},
|
||||
{
|
||||
"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 }) }}",
|
||||
"options": {
|
||||
"responseCode": 422
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
560
|
||||
],
|
||||
"id": "0fe0e610-b892-5a76-bf63-7d8e0bb39480"
|
||||
},
|
||||
{
|
||||
"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": "7a40a393-288a-53ae-a82e-b98d947ee93b"
|
||||
},
|
||||
{
|
||||
"name": "Only passed",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "return $input.all().filter((i) => i.json.ok);"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
300
|
||||
],
|
||||
"id": "eb586a0d-12f4-5fde-9d68-043c55aef1f4"
|
||||
},
|
||||
{
|
||||
"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": "00774c60-9e1b-5b13-9372-05bfff2c5e5f"
|
||||
},
|
||||
{
|
||||
"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": "b9d81b74-5416-5c1f-83d5-27198a9c3e4c"
|
||||
},
|
||||
{
|
||||
"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": "efbd675d-b385-598c-8d25-59047ac9b1ec"
|
||||
},
|
||||
{
|
||||
"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": "31549ecc-3322-5f92-a41e-c3006f0b0421"
|
||||
},
|
||||
{
|
||||
"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) } }];"
|
||||
},
|
||||
"position": [
|
||||
1980,
|
||||
300
|
||||
],
|
||||
"id": "b07acdff-4b76-5814-84cc-bd6011916683"
|
||||
},
|
||||
{
|
||||
"name": "Send result",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
2200,
|
||||
300
|
||||
],
|
||||
"id": "e1ac883e-0b0e-5106-b01e-7fb7363f9c74"
|
||||
}
|
||||
],
|
||||
"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": "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": "Send result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
{
|
||||
"name": "78 Schema check (reference)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Check webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-check",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
],
|
||||
"id": "c042a07f-a8f7-565f-b562-64bdc1e951e1",
|
||||
"webhookId": "a46fe9fe-4604-5c34-8435-2d87f4063861"
|
||||
},
|
||||
{
|
||||
"name": "Get schema",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "=http://data-api:8800/schemas/{{ encodeURIComponent($json.body.schema ?? '') }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"id": "e7a886c9-f582-578b-bb91-75b1b0464de0"
|
||||
},
|
||||
{
|
||||
"name": "Known schema?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.fields !== undefined }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"id": "368be2eb-9471-523a-88c7-adb0d427375b"
|
||||
},
|
||||
{
|
||||
"name": "Unknown schema",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ JSON.stringify({ error: 'unknown schema', schema: $('Check webhook').first().json.body.schema ?? null }) }}",
|
||||
"options": {
|
||||
"responseCode": 400
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
560
|
||||
],
|
||||
"id": "482f3c8d-6a2c-5b9a-9f5c-056b8bf51986"
|
||||
},
|
||||
{
|
||||
"name": "Check rows",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const body = $('Check webhook').first().json.body;\nconst schema = $json;\nlet items = body.items;\nif (typeof body.csv === 'string') {\n const [head, ...lines] = body.csv.trim().split(/\\r?\\n/);\n const cols = head.split(',').map((c) => c.trim());\n items = lines.map((line) => Object.fromEntries(line.split(',').map((v, i) => [cols[i], v.trim()])));\n}\nconst isDate = (v) => { if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(v)) return false; const d = new Date(v + 'T00:00:00Z'); return !isNaN(d) && d.toISOString().slice(0, 10) === v; };\nconst seen = new Set();\nconst rows = (Array.isArray(items) ? items : []).map((r, index) => {\n const errors = [];\n for (const [name, f] of Object.entries(schema.fields)) {\n const v = r?.[name];\n const empty = v === undefined || v === null || (typeof v === 'string' && v.trim() === '') || (Array.isArray(v) && v.length === 0);\n if (empty) { if (f.required) errors.push(`missing:${name}`); continue; }\n const s = String(v);\n if (f.type === 'string') { if (f.max && s.length > f.max) errors.push(`too_long:${name}`); if (f.min && s.length < f.min) errors.push(`too_short:${name}`); }\n if (f.type === 'enum' && !f.values.includes(s)) errors.push(`not_allowed:${name}`);\n if (f.type === 'email' && !/^[\\w.+-]+@[\\w-]+(\\.[\\w-]+)+$/.test(s)) errors.push(`bad_format:${name}`);\n if (f.type === 'phone' && !/^0\\d{1,2}-\\d{3,4}-\\d{4}$/.test(s)) errors.push(`bad_format:${name}`);\n if (f.type === 'date' && !isDate(s)) errors.push(`bad_format:${name}`);\n if (f.type === 'pattern' && !new RegExp(f.pattern).test(s)) errors.push(`bad_format:${name}`);\n if (f.type === 'list') { if (!Array.isArray(v)) errors.push(`bad_format:${name}`); else { if (f.min_items && v.length < f.min_items) errors.push(`too_few:${name}`); if (f.max_items && v.length > f.max_items) errors.push(`too_many:${name}`); } }\n }\n const key = String(r?.[schema.key] ?? '');\n if (key && seen.has(key)) errors.push('duplicate_in_batch');\n if (key) seen.add(key);\n return { row: index + 1, key, ok: errors.length === 0, errors };\n});\nconst codes = {};\nfor (const row of rows) for (const e of row.errors) codes[e] = (codes[e] ?? 0) + 1;\nconst valid = rows.filter((r) => r.ok).length;\nreturn [{ json: { schema: schema.name, batch: body.batch ?? '', total: rows.length, valid, invalid: rows.length - valid, rows, codes } }];"
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
],
|
||||
"id": "d5d18640-aebb-5227-905c-92435b892892"
|
||||
},
|
||||
{
|
||||
"name": "Send result",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
300
|
||||
],
|
||||
"id": "3cf4a033-8e67-5be5-8392-473991da1f4d"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Check webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get schema",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get schema": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Known schema?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Known schema?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Check rows",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Unknown schema",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check rows": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,676 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user