77일차 실행 기록 분석: 실습 15개 · 강의(개요 30장 · 단계 문서 · 4단계 정리 · 05 바이브), 정답본 LAB-A · answers · VIBE-A · tlog 포함
This commit is contained in:
1 parent
036eae1c6a
commit
dcd4cdd66b
148 files changed
+40344
No files matched your search
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"name": "77 Log report (team baseline, provided)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Report webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "base-report",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get logs",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/logs/query",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ source: 'all' }) }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Compute metrics",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "// The team's old report: written in a hurry last month. Compare its numbers with the definitions in AGENTS.md 3.\nconst body = $('Report webhook').first().json.body ?? {};\nconst from = body.from ?? '', to = body.to ?? '';\nconst rows = ($json.logs ?? []).filter((r) => (!from || r.started_at >= from) && (!to || r.started_at < to));\nconst errors = rows.filter((r) => r.status === 'error').length;\nconst by_route = {};\nfor (const r of rows) if (r.route) by_route[r.route] = (by_route[r.route] ?? 0) + 1;\nconst avg_ms = rows.reduce((a, r) => a + r.duration_ms, 0) / rows.length;\nreturn [{ json: { total: rows.length, errors, error_rate: (errors / rows.length) * 100, avg_ms, by_route } }];"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send report",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Report webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get logs",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get logs": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Compute metrics",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Compute metrics": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send report",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
{
|
||||
"name": "77 Live helpdesk (provided)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Question webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-helpdesk",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"webhookId": "de518e93-ed19-541f-a6fc-687a4efaf1b2",
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
],
|
||||
"id": "c92ed07d-21e3-5441-bf1f-7547c58d0804"
|
||||
},
|
||||
{
|
||||
"name": "Topic",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const q = String($json.body.question ?? '');\nconst table = [['접수 현황', /T-?\\d{4}/i], ['와이파이', /와이파이|wi-?fi/i], ['VPN', /vpn|원격|재택/i], ['비밀번호', /비밀번호|패스워드/i],\n ['프린터', /프린터|인쇄/i], ['메일', /메일|첨부/i], ['설치', /설치|라이선스|프로그램/i]];\nconst hit = table.find(([, re]) => re.test(q));\nreturn [{ json: { topic: hit ? hit[0] : '기타', t0: Date.now() } }];"
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"id": "88961978-e8ec-5726-a23d-67ca6574360c"
|
||||
},
|
||||
{
|
||||
"name": "Search FAQ",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/faq",
|
||||
"options": {
|
||||
"timeout": 1000
|
||||
},
|
||||
"sendQuery": true,
|
||||
"queryParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "q",
|
||||
"value": "={{ $('Question webhook').first().json.body.question }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"onError": "continueErrorOutput",
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"id": "23b65ea5-d9d2-5781-a93c-aa1d5e0ad959"
|
||||
},
|
||||
{
|
||||
"name": "FAQ done",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "return [{ json: { ...$json, faq_ms: Date.now() - $('Topic').first().json.t0 } }];"
|
||||
},
|
||||
"position": [
|
||||
550,
|
||||
180
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Found?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ ($json.results ?? []).length > 0 }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
],
|
||||
"id": "ebc6810e-e76b-5221-89bb-4c0eb9fad932"
|
||||
},
|
||||
{
|
||||
"name": "Answer",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "=질문: {{ $('Question webhook').first().json.body.question }}\nFAQ: {{ JSON.stringify($('Search FAQ').first().json.results) }}",
|
||||
"options": {
|
||||
"systemMessage": "You are the company IT help desk. Answer in Korean in at most 3 sentences using only the FAQ entries given. Never invent steps. Use 합니다체.",
|
||||
"maxIterations": 1
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
200
|
||||
],
|
||||
"id": "2ee0f4ec-9cdf-507b-aa32-64e9c8f475ef"
|
||||
},
|
||||
{
|
||||
"name": "Answer 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": [
|
||||
880,
|
||||
420
|
||||
],
|
||||
"id": "1cf57665-a16a-5f55-94b1-732f34b30d91"
|
||||
},
|
||||
{
|
||||
"name": "Answered",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const body = $('Question webhook').first().json.body;\nconst t = $('Topic').first().json;\nconst base = { execution_id: Number($execution.id), workflow: 'helpdesk', user: body.user ?? '', topic: t.topic, question: body.question ?? '', release: '1.1.0',\n channel: 'stable', repeat: false, tag: body.tag ?? '', duration_ms: Date.now() - t.t0, cost_usd: 0 };\nconst faq_ms = $('FAQ done').first().json.faq_ms;\nbase.steps = { faq_ms, answer_ms: Math.max(0, base.duration_ms - faq_ms) };\nreturn [{ json: { ...base, status: 'success', error_node: null, error_message: null, route: '자동 답변', llm_calls: 1, answer: String($json.output ?? '').trim() } }];"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
200
|
||||
],
|
||||
"id": "6db09299-84d1-50b1-800f-c074ccb0d3ed"
|
||||
},
|
||||
{
|
||||
"name": "Not found",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const body = $('Question webhook').first().json.body;\nconst t = $('Topic').first().json;\nconst base = { execution_id: Number($execution.id), workflow: 'helpdesk', user: body.user ?? '', topic: t.topic, question: body.question ?? '', release: '1.1.0',\n channel: 'stable', repeat: false, tag: body.tag ?? '', duration_ms: Date.now() - t.t0, cost_usd: 0 };\nconst faq_ms = $('FAQ done').first().json.faq_ms;\nbase.steps = { faq_ms, answer_ms: 0 };\nreturn [{ json: { ...base, status: 'success', error_node: null, error_message: null, route: '담당자 연결', llm_calls: 0, answer: 'FAQ에서 찾지 못해 담당자에게 연결합니다.' } }];"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
400
|
||||
],
|
||||
"id": "33f61379-1764-55c7-aff5-3521b1cf451b"
|
||||
},
|
||||
{
|
||||
"name": "Failed",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const body = $('Question webhook').first().json.body;\nconst t = $('Topic').first().json;\nconst base = { execution_id: Number($execution.id), workflow: 'helpdesk', user: body.user ?? '', topic: t.topic, question: body.question ?? '', release: '1.1.0',\n channel: 'stable', repeat: false, tag: body.tag ?? '', duration_ms: Date.now() - t.t0, cost_usd: 0 };\nbase.steps = { faq_ms: base.duration_ms, answer_ms: 0 };\nconst message = String($json.error?.message ?? $json.error ?? 'error').slice(0, 120);\nreturn [{ json: { ...base, status: 'error', error_node: 'Search FAQ', error_message: message, route: null, llm_calls: 0, answer: '지금은 답할 수 없어 담당자에게 연결합니다.' } }];"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
600
|
||||
],
|
||||
"id": "6a62a108-4c6c-57fa-b444-3c0b72f85597"
|
||||
},
|
||||
{
|
||||
"name": "Log Answered",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/logs",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ items: [(({ answer, ...rest }) => rest)($json)] }) }}"
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
200
|
||||
],
|
||||
"id": "cc12c3ab-8643-571d-83f4-d27c09dd8e5a"
|
||||
},
|
||||
{
|
||||
"name": "Reply Answered",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const r = $('Answered').first().json;\nreturn [{ json: { route: r.route ?? '담당자 연결', status: r.status, topic: r.topic, answer: r.answer, error: r.error_message } }];"
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
200
|
||||
],
|
||||
"id": "5c5ffce6-1941-59af-bc8a-5071809d0656"
|
||||
},
|
||||
{
|
||||
"name": "Send Answered",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
200
|
||||
],
|
||||
"id": "1c9e1737-ae60-5754-abbb-2bfab0c7e9b7"
|
||||
},
|
||||
{
|
||||
"name": "Log Not found",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/logs",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ items: [(({ answer, ...rest }) => rest)($json)] }) }}"
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
400
|
||||
],
|
||||
"id": "055e9419-007c-5f4d-b4a6-e70b34ff2a24"
|
||||
},
|
||||
{
|
||||
"name": "Reply Not found",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const r = $('Not found').first().json;\nreturn [{ json: { route: r.route ?? '담당자 연결', status: r.status, topic: r.topic, answer: r.answer, error: r.error_message } }];"
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
400
|
||||
],
|
||||
"id": "dc79daf9-8f4d-506b-a68b-00f3fd4d44c9"
|
||||
},
|
||||
{
|
||||
"name": "Send Not found",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
400
|
||||
],
|
||||
"id": "df1457bb-ec41-5c08-b3ff-f2284a6e1401"
|
||||
},
|
||||
{
|
||||
"name": "Log Failed",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/logs",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ items: [(({ answer, ...rest }) => rest)($json)] }) }}"
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
600
|
||||
],
|
||||
"id": "760aaaca-e80d-5fe4-9e01-c3772483123d"
|
||||
},
|
||||
{
|
||||
"name": "Reply Failed",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const r = $('Failed').first().json;\nreturn [{ json: { route: r.route ?? '담당자 연결', status: r.status, topic: r.topic, answer: r.answer, error: r.error_message } }];"
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
600
|
||||
],
|
||||
"id": "addce3eb-ad29-54ac-95c4-87fd3aafb3fc"
|
||||
},
|
||||
{
|
||||
"name": "Send Failed",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
600
|
||||
],
|
||||
"id": "3a330204-2c41-5c6c-93f1-f39b0b04dc0d"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Question webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Topic",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Topic": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Search FAQ",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Search FAQ": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "FAQ done",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Failed",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Found?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Answer",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Not found",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Answer model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Answer",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Answer": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Answered",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Answered": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Log Answered",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Log Answered": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Reply Answered",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Reply Answered": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send Answered",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Not found": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Log Not found",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Log Not found": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Reply Not found",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Reply Not found": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send Not found",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Failed": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Log Failed",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Log Failed": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Reply Failed",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Reply Failed": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send Failed",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"FAQ done": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Found?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"name": "77 Log report (reference)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Report webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-log-report",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get logs",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/logs/query",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ from: $json.body.from ?? '', to: $json.body.to ?? '', tag: $json.body.tag ?? '', source: $json.body.source ?? 'all' }) }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Compute metrics",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const pct = (vals, p) => { const s = [...vals].sort((a, b) => a - b); return s.length ? s[Math.max(0, Math.ceil(p * s.length) - 1)] : 0; };\nconst rate = (a, b) => (b ? Math.round((a / b) * 10000) / 10000 : 0);\nconst rows = $json.logs ?? [];\nconst errors = rows.filter((r) => r.status === 'error').length;\nconst by_route = {};\nfor (const r of rows) { const k = r.route ?? '오류'; by_route[k] = (by_route[k] ?? 0) + 1; }\nconst d = rows.map((r) => r.duration_ms);\nconst out = { total: rows.length, errors, error_rate: rate(errors, rows.length), p50_ms: pct(d, 0.5), p95_ms: pct(d, 0.95), by_route };\nreturn [{ json: out }];"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send report",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Report webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get logs",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get logs": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Compute metrics",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Compute metrics": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send report",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"name": "77 Log report + failures and unanswered topics (reference)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Report webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-log-report",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get logs",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/logs/query",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ from: $json.body.from ?? '', to: $json.body.to ?? '', tag: $json.body.tag ?? '', source: $json.body.source ?? 'all' }) }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Compute metrics",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const pct = (vals, p) => { const s = [...vals].sort((a, b) => a - b); return s.length ? s[Math.max(0, Math.ceil(p * s.length) - 1)] : 0; };\nconst rate = (a, b) => (b ? Math.round((a / b) * 10000) / 10000 : 0);\nconst rows = $json.logs ?? [];\nconst errors = rows.filter((r) => r.status === 'error').length;\nconst by_route = {};\nfor (const r of rows) { const k = r.route ?? '오류'; by_route[k] = (by_route[k] ?? 0) + 1; }\nconst d = rows.map((r) => r.duration_ms);\nconst out = { total: rows.length, errors, error_rate: rate(errors, rows.length), p50_ms: pct(d, 0.5), p95_ms: pct(d, 0.95), by_route };\nconst group = (key) => { const g = {}; for (const r of rows) (g[key(r)] ??= []).push(r); return g; };\nout.by_day = Object.fromEntries(Object.entries(group((r) => r.started_at.slice(0, 10))).sort().map(([k, v]) => {\n const e = v.filter((r) => r.status === 'error').length; return [k, { total: v.length, errors: e, error_rate: rate(e, v.length) }]; }));\nout.by_release = {};\nfor (const r of rows.filter((x) => x.route === '자동 답변')) { const b = (out.by_release[r.release] ??= { answers: 0, repeats: 0 }); b.answers += 1; if (r.repeat) b.repeats += 1; }\nfor (const b of Object.values(out.by_release)) b.repeat_rate = rate(b.repeats, b.answers);\nout.steps = {};\nfor (const k of ['faq_ms', 'answer_ms']) { const v = rows.map((r) => r.steps?.[k] ?? 0).filter((x) => x > 0); out.steps[k] = { runs: v.length, p50: pct(v, 0.5), p95: pct(v, 0.95) }; }\nout.cost_usd = Math.round(rows.reduce((a, r) => a + (r.cost_usd ?? 0), 0) * 100000) / 100000;\nout.cost_per_run = rows.length ? Math.round((out.cost_usd / rows.length) * 1000000) / 1000000 : 0;\nout.llm_calls = rows.reduce((a, r) => a + (r.llm_calls ?? 0), 0);\nconst fails = {};\nfor (const r of rows.filter((x) => x.status === 'error')) { const k = `${r.error_node}|${r.error_message}`; fails[k] = (fails[k] ?? 0) + 1; }\nout.failures = Object.entries(fails).map(([k, count]) => ({ node: k.split('|')[0], message: k.split('|')[1], count })).sort((a, b) => b.count - a.count);\nconst un = {};\nfor (const r of rows.filter((x) => x.route === '담당자 연결')) un[r.topic] = (un[r.topic] ?? 0) + 1;\nout.unanswered_topics = Object.entries(un).map(([topic, count]) => ({ topic, count })).sort((a, b) => b.count - a.count);\nreturn [{ json: out }];"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send report",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Report webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get logs",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get logs": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Compute metrics",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Compute metrics": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send report",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"name": "77 Log report + anomalies (reference)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Report webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-log-report",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get logs",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/logs/query",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ from: $json.body.from ?? '', to: $json.body.to ?? '', tag: $json.body.tag ?? '', source: $json.body.source ?? 'all' }) }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Compute metrics",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const pct = (vals, p) => { const s = [...vals].sort((a, b) => a - b); return s.length ? s[Math.max(0, Math.ceil(p * s.length) - 1)] : 0; };\nconst rate = (a, b) => (b ? Math.round((a / b) * 10000) / 10000 : 0);\nconst rows = $json.logs ?? [];\nconst errors = rows.filter((r) => r.status === 'error').length;\nconst by_route = {};\nfor (const r of rows) { const k = r.route ?? '오류'; by_route[k] = (by_route[k] ?? 0) + 1; }\nconst d = rows.map((r) => r.duration_ms);\nconst out = { total: rows.length, errors, error_rate: rate(errors, rows.length), p50_ms: pct(d, 0.5), p95_ms: pct(d, 0.95), by_route };\nconst group = (key) => { const g = {}; for (const r of rows) (g[key(r)] ??= []).push(r); return g; };\nout.by_day = Object.fromEntries(Object.entries(group((r) => r.started_at.slice(0, 10))).sort().map(([k, v]) => {\n const e = v.filter((r) => r.status === 'error').length; return [k, { total: v.length, errors: e, error_rate: rate(e, v.length) }]; }));\nout.by_release = {};\nfor (const r of rows.filter((x) => x.route === '자동 답변')) { const b = (out.by_release[r.release] ??= { answers: 0, repeats: 0 }); b.answers += 1; if (r.repeat) b.repeats += 1; }\nfor (const b of Object.values(out.by_release)) b.repeat_rate = rate(b.repeats, b.answers);\nout.steps = {};\nfor (const k of ['faq_ms', 'answer_ms']) { const v = rows.map((r) => r.steps?.[k] ?? 0).filter((x) => x > 0); out.steps[k] = { runs: v.length, p50: pct(v, 0.5), p95: pct(v, 0.95) }; }\nout.cost_usd = Math.round(rows.reduce((a, r) => a + (r.cost_usd ?? 0), 0) * 100000) / 100000;\nout.cost_per_run = rows.length ? Math.round((out.cost_usd / rows.length) * 1000000) / 1000000 : 0;\nout.llm_calls = rows.reduce((a, r) => a + (r.llm_calls ?? 0), 0);\nconst fails = {};\nfor (const r of rows.filter((x) => x.status === 'error')) { const k = `${r.error_node}|${r.error_message}`; fails[k] = (fails[k] ?? 0) + 1; }\nout.failures = Object.entries(fails).map(([k, count]) => ({ node: k.split('|')[0], message: k.split('|')[1], count })).sort((a, b) => b.count - a.count);\nconst un = {};\nfor (const r of rows.filter((x) => x.route === '담당자 연결')) un[r.topic] = (un[r.topic] ?? 0) + 1;\nout.unanswered_topics = Object.entries(un).map(([topic, count]) => ({ topic, count })).sort((a, b) => b.count - a.count);\nconst hours = {};\nfor (const r of rows) (hours[r.started_at.slice(0, 13)] ??= []).push(r);\nout.anomalies = Object.entries(hours).sort().map(([hour, v]) => { const e = v.filter((r) => r.status === 'error').length; return { hour, total: v.length, errors: e, error_rate: rate(e, v.length) }; })\n .filter((h) => h.total >= 5 && h.error_rate >= 3 * out.error_rate && h.errors > 0);\nreturn [{ json: out }];"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send report",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Report webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get logs",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get logs": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Compute metrics",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Compute metrics": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send report",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,544 @@
|
||||
{
|
||||
"name": "77 Live helpdesk fixed (reference)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Question webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-helpdesk",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"webhookId": "de518e93-ed19-541f-a6fc-687a4efaf1b2",
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
],
|
||||
"id": "c92ed07d-21e3-5441-bf1f-7547c58d0804"
|
||||
},
|
||||
{
|
||||
"name": "Topic",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const q = String($json.body.question ?? '');\nconst table = [['접수 현황', /T-?\\d{4}/i], ['와이파이', /와이파이|wi-?fi/i], ['VPN', /vpn|원격|재택/i], ['비밀번호', /비밀번호|패스워드/i],\n ['프린터', /프린터|인쇄/i], ['메일', /메일|첨부/i], ['설치', /설치|라이선스|프로그램/i]];\nconst hit = table.find(([, re]) => re.test(q));\nreturn [{ json: { topic: hit ? hit[0] : '기타', t0: Date.now() } }];"
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"id": "88961978-e8ec-5726-a23d-67ca6574360c"
|
||||
},
|
||||
{
|
||||
"name": "Search FAQ",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/faq",
|
||||
"options": {
|
||||
"timeout": 3000
|
||||
},
|
||||
"sendQuery": true,
|
||||
"queryParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "q",
|
||||
"value": "={{ $('Question webhook').first().json.body.question }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"onError": "continueErrorOutput",
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"id": "23b65ea5-d9d2-5781-a93c-aa1d5e0ad959",
|
||||
"retryOnFail": true,
|
||||
"maxTries": 2,
|
||||
"waitBetweenTries": 300
|
||||
},
|
||||
{
|
||||
"name": "FAQ done",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "return [{ json: { ...$json, faq_ms: Date.now() - $('Topic').first().json.t0 } }];"
|
||||
},
|
||||
"position": [
|
||||
550,
|
||||
180
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Found?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ ($json.results ?? []).length > 0 }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
],
|
||||
"id": "ebc6810e-e76b-5221-89bb-4c0eb9fad932"
|
||||
},
|
||||
{
|
||||
"name": "Answer",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "=질문: {{ $('Question webhook').first().json.body.question }}\nFAQ: {{ JSON.stringify($('Search FAQ').first().json.results) }}",
|
||||
"options": {
|
||||
"systemMessage": "You are the company IT help desk. Answer in Korean in at most 3 sentences using only the FAQ entries given. Never invent steps. Use 합니다체.",
|
||||
"maxIterations": 1
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
200
|
||||
],
|
||||
"id": "2ee0f4ec-9cdf-507b-aa32-64e9c8f475ef"
|
||||
},
|
||||
{
|
||||
"name": "Answer 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": [
|
||||
880,
|
||||
420
|
||||
],
|
||||
"id": "1cf57665-a16a-5f55-94b1-732f34b30d91"
|
||||
},
|
||||
{
|
||||
"name": "Answered",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const body = $('Question webhook').first().json.body;\nconst t = $('Topic').first().json;\nconst base = { execution_id: Number($execution.id), workflow: 'helpdesk', user: body.user ?? '', topic: t.topic, question: body.question ?? '', release: '1.1.0',\n channel: 'stable', repeat: false, tag: body.tag ?? '', duration_ms: Date.now() - t.t0, cost_usd: 0 };\nconst faq_ms = $('FAQ done').first().json.faq_ms;\nbase.steps = { faq_ms, answer_ms: Math.max(0, base.duration_ms - faq_ms) };\nreturn [{ json: { ...base, status: 'success', error_node: null, error_message: null, route: '자동 답변', llm_calls: 1, answer: String($json.output ?? '').trim() } }];"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
200
|
||||
],
|
||||
"id": "6db09299-84d1-50b1-800f-c074ccb0d3ed"
|
||||
},
|
||||
{
|
||||
"name": "Not found",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const body = $('Question webhook').first().json.body;\nconst t = $('Topic').first().json;\nconst base = { execution_id: Number($execution.id), workflow: 'helpdesk', user: body.user ?? '', topic: t.topic, question: body.question ?? '', release: '1.1.0',\n channel: 'stable', repeat: false, tag: body.tag ?? '', duration_ms: Date.now() - t.t0, cost_usd: 0 };\nconst faq_ms = $('FAQ done').first().json.faq_ms;\nbase.steps = { faq_ms, answer_ms: 0 };\nreturn [{ json: { ...base, status: 'success', error_node: null, error_message: null, route: '담당자 연결', llm_calls: 0, answer: 'FAQ에서 찾지 못해 담당자에게 연결합니다.' } }];"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
400
|
||||
],
|
||||
"id": "33f61379-1764-55c7-aff5-3521b1cf451b"
|
||||
},
|
||||
{
|
||||
"name": "Failed",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const body = $('Question webhook').first().json.body;\nconst t = $('Topic').first().json;\nconst base = { execution_id: Number($execution.id), workflow: 'helpdesk', user: body.user ?? '', topic: t.topic, question: body.question ?? '', release: '1.1.0',\n channel: 'stable', repeat: false, tag: body.tag ?? '', duration_ms: Date.now() - t.t0, cost_usd: 0 };\nbase.steps = { faq_ms: base.duration_ms, answer_ms: 0 };\nconst message = String($json.error?.message ?? $json.error ?? 'error').slice(0, 120);\nreturn [{ json: { ...base, status: 'error', error_node: 'Search FAQ', error_message: message, route: null, llm_calls: 0, answer: '지금은 답할 수 없어 담당자에게 연결합니다.' } }];"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
600
|
||||
],
|
||||
"id": "6a62a108-4c6c-57fa-b444-3c0b72f85597"
|
||||
},
|
||||
{
|
||||
"name": "Log Answered",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/logs",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ items: [(({ answer, ...rest }) => rest)($json)] }) }}"
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
200
|
||||
],
|
||||
"id": "cc12c3ab-8643-571d-83f4-d27c09dd8e5a"
|
||||
},
|
||||
{
|
||||
"name": "Reply Answered",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const r = $('Answered').first().json;\nreturn [{ json: { route: r.route ?? '담당자 연결', status: r.status, topic: r.topic, answer: r.answer, error: r.error_message } }];"
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
200
|
||||
],
|
||||
"id": "5c5ffce6-1941-59af-bc8a-5071809d0656"
|
||||
},
|
||||
{
|
||||
"name": "Send Answered",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
200
|
||||
],
|
||||
"id": "1c9e1737-ae60-5754-abbb-2bfab0c7e9b7"
|
||||
},
|
||||
{
|
||||
"name": "Log Not found",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/logs",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ items: [(({ answer, ...rest }) => rest)($json)] }) }}"
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
400
|
||||
],
|
||||
"id": "055e9419-007c-5f4d-b4a6-e70b34ff2a24"
|
||||
},
|
||||
{
|
||||
"name": "Reply Not found",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const r = $('Not found').first().json;\nreturn [{ json: { route: r.route ?? '담당자 연결', status: r.status, topic: r.topic, answer: r.answer, error: r.error_message } }];"
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
400
|
||||
],
|
||||
"id": "dc79daf9-8f4d-506b-a68b-00f3fd4d44c9"
|
||||
},
|
||||
{
|
||||
"name": "Send Not found",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
400
|
||||
],
|
||||
"id": "df1457bb-ec41-5c08-b3ff-f2284a6e1401"
|
||||
},
|
||||
{
|
||||
"name": "Log Failed",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/logs",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ items: [(({ answer, ...rest }) => rest)($json)] }) }}"
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
600
|
||||
],
|
||||
"id": "760aaaca-e80d-5fe4-9e01-c3772483123d"
|
||||
},
|
||||
{
|
||||
"name": "Reply Failed",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const r = $('Failed').first().json;\nreturn [{ json: { route: r.route ?? '담당자 연결', status: r.status, topic: r.topic, answer: r.answer, error: r.error_message } }];"
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
600
|
||||
],
|
||||
"id": "addce3eb-ad29-54ac-95c4-87fd3aafb3fc"
|
||||
},
|
||||
{
|
||||
"name": "Send Failed",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
600
|
||||
],
|
||||
"id": "3a330204-2c41-5c6c-93f1-f39b0b04dc0d"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Question webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Topic",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Topic": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Search FAQ",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Search FAQ": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "FAQ done",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Failed",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Found?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Answer",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Not found",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Answer model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Answer",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Answer": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Answered",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Answered": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Log Answered",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Log Answered": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Reply Answered",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Reply Answered": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send Answered",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Not found": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Log Not found",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Log Not found": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Reply Not found",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Reply Not found": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send Not found",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Failed": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Log Failed",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Log Failed": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Reply Failed",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Reply Failed": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send Failed",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"FAQ done": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Found?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"name": "77 Log report + SLO (reference)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Report webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-log-report",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get logs",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/logs/query",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ from: $json.body.from ?? '', to: $json.body.to ?? '', tag: $json.body.tag ?? '', source: $json.body.source ?? 'all' }) }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Compute metrics",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const pct = (vals, p) => { const s = [...vals].sort((a, b) => a - b); return s.length ? s[Math.max(0, Math.ceil(p * s.length) - 1)] : 0; };\nconst rate = (a, b) => (b ? Math.round((a / b) * 10000) / 10000 : 0);\nconst rows = $json.logs ?? [];\nconst errors = rows.filter((r) => r.status === 'error').length;\nconst by_route = {};\nfor (const r of rows) { const k = r.route ?? '오류'; by_route[k] = (by_route[k] ?? 0) + 1; }\nconst d = rows.map((r) => r.duration_ms);\nconst out = { total: rows.length, errors, error_rate: rate(errors, rows.length), p50_ms: pct(d, 0.5), p95_ms: pct(d, 0.95), by_route };\nconst group = (key) => { const g = {}; for (const r of rows) (g[key(r)] ??= []).push(r); return g; };\nout.by_day = Object.fromEntries(Object.entries(group((r) => r.started_at.slice(0, 10))).sort().map(([k, v]) => {\n const e = v.filter((r) => r.status === 'error').length; return [k, { total: v.length, errors: e, error_rate: rate(e, v.length) }]; }));\nout.by_release = {};\nfor (const r of rows.filter((x) => x.route === '자동 답변')) { const b = (out.by_release[r.release] ??= { answers: 0, repeats: 0 }); b.answers += 1; if (r.repeat) b.repeats += 1; }\nfor (const b of Object.values(out.by_release)) b.repeat_rate = rate(b.repeats, b.answers);\nout.steps = {};\nfor (const k of ['faq_ms', 'answer_ms']) { const v = rows.map((r) => r.steps?.[k] ?? 0).filter((x) => x > 0); out.steps[k] = { runs: v.length, p50: pct(v, 0.5), p95: pct(v, 0.95) }; }\nout.cost_usd = Math.round(rows.reduce((a, r) => a + (r.cost_usd ?? 0), 0) * 100000) / 100000;\nout.cost_per_run = rows.length ? Math.round((out.cost_usd / rows.length) * 1000000) / 1000000 : 0;\nout.llm_calls = rows.reduce((a, r) => a + (r.llm_calls ?? 0), 0);\nconst fails = {};\nfor (const r of rows.filter((x) => x.status === 'error')) { const k = `${r.error_node}|${r.error_message}`; fails[k] = (fails[k] ?? 0) + 1; }\nout.failures = Object.entries(fails).map(([k, count]) => ({ node: k.split('|')[0], message: k.split('|')[1], count })).sort((a, b) => b.count - a.count);\nconst un = {};\nfor (const r of rows.filter((x) => x.route === '담당자 연결')) un[r.topic] = (un[r.topic] ?? 0) + 1;\nout.unanswered_topics = Object.entries(un).map(([topic, count]) => ({ topic, count })).sort((a, b) => b.count - a.count);\nconst hours = {};\nfor (const r of rows) (hours[r.started_at.slice(0, 13)] ??= []).push(r);\nout.anomalies = Object.entries(hours).sort().map(([hour, v]) => { const e = v.filter((r) => r.status === 'error').length; return { hour, total: v.length, errors: e, error_rate: rate(e, v.length) }; })\n .filter((h) => h.total >= 5 && h.error_rate >= 3 * out.error_rate && h.errors > 0);\nconst rep = {};\nfor (const r of rows.filter((x) => x.route === '자동 답변')) { const t = (rep[r.topic] ??= { topic: r.topic, answers: 0, repeats: 0 }); t.answers += 1; if (r.repeat) t.repeats += 1; }\nout.repeat_topics = Object.values(rep).filter((t) => t.repeats > 0).map((t) => ({ ...t, repeat_rate: rate(t.repeats, t.answers) }))\n .sort((a, b) => b.repeat_rate - a.repeat_rate || b.repeats - a.repeats);\nconst body = $('Report webhook').first().json.body ?? {};\nconst limits = { error_rate: 0.01, p95_ms: 3000, unanswered_rate: 0.02, ...(body.slo ?? {}) };\nout.unanswered_rate = rate(rows.filter((r) => r.route === '담당자 연결').length, rows.length);\nconst breaches = ['error_rate', 'p95_ms', 'unanswered_rate'].filter((k) => out[k] > limits[k]);\nout.slo = { ok: breaches.length === 0, breaches, limits };\nreturn [{ json: out }];"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send report",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Report webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get logs",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get logs": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Compute metrics",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Compute metrics": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send report",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user