Files
aio-01d4-stu/Last3Mon/79/reference/lab03-example2-brief.json
T

199 lines
7.5 KiB
JSON

{
"name": "79 Ops brief + summary (reference)",
"nodes": [
{
"name": "Brief webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"parameters": {
"httpMethod": "POST",
"path": "my-ops-brief",
"responseMode": "responseNode",
"options": {}
},
"webhookId": "d8018a49-2915-5dd1-adc6-aa234e759aa9",
"position": [
0,
300
],
"id": "4c8cf041-481f-5b3f-bda4-66b2e588a2ff"
},
{
"name": "Get facts",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://localhost:5678/webhook/my-ops-facts",
"options": {
"timeout": 30000
},
"method": "POST",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ from: $json.body?.from ?? '', to: $json.body?.to ?? '' }) }}"
},
"position": [
220,
300
],
"id": "a841ddde-8f4b-56c1-ae8a-c81c8d9d1690"
},
{
"name": "Make brief",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const f = $('Get facts').first().json;\nconst label = { runs: '실행', alerts: '알림', dlq: 'DLQ', quality: '데이터 품질', cost: '비용', release: '릴리스' };\nconst mark = { red: '🔴', yellow: '🟡', green: '🟢', unknown: '⚪' };\nconst actionsFor = {\"runs\": \"오류 묶음 1위 확인 · 고치기 (77일차)\", \"alerts\": \"높은 알림부터 확인하고 닫기 (76일차)\", \"dlq\": \"parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\", \"quality\": \"격리함을 콘텐츠 담당자에게 (78일차)\", \"cost\": \"비용 1위 사용자 · 한도 확인 (75일차)\", \"release\": \"카나리 끄기 · 되돌리기 검토 (74일차)\"};\nconst pct = (v) => (Math.round(v * 10000) / 100) + '%';\nconst g = (k, fn) => (f[k] === null ? '출처 응답 없음' : fn());\nconst key = {\n runs: g('runs', () => `오류율 ${pct(f.runs.error_rate)} · p95 ${f.runs.p95_ms}ms · 답 못 함 ${pct(f.runs.unanswered_rate)}`),\n alerts: g('alerts', () => `${f.alerts.count}건 (높음 ${f.alerts.high})`),\n dlq: g('dlq', () => `${f.dlq.size}건 (parked ${f.dlq.parked})`),\n quality: g('quality', () => `평균 ${f.quality.avg_score} · 마지막 ${f.quality.last_score} · 격리 ${f.quality.quarantine}`),\n cost: g('cost', () => `$${f.cost.cost_usd} / $${f.cost.daily_usd} (${pct(f.cost.used_ratio)})`),\n release: g('release', () => `${f.release.active}` + (f.release.canary_release ? ` · 카나리 ${f.release.canary_release} ${f.release.canary_percent}% (다시 묻기 ${f.release.canary_repeat_rate === null ? '-' : pct(f.release.canary_repeat_rate)})` : '')),\n};\nconst order = ['runs', 'alerts', 'dlq', 'quality', 'cost', 'release'];\nconst focus = [...f.red, ...f.yellow].slice(0, 3);\nconst actions = focus.map((k) => ({ area: k, status: f.health[k], action: actionsFor[k] }));\nconst lines = [\n `# 운영 브리핑 (${f.period.from} ~ ${f.period.to} 실행 기록 + 지금 상태)`, '',\n `전체: ${mark[f.overall]} ${f.overall}`, '',\n '| 영역 | 상태 | 숫자 |', '| --- | --- | --- |',\n ...order.map((k) => `| ${label[k]} | ${mark[f.health[k]]} | ${key[k]} |`), '',\n '## 먼저 볼 것', '',\n ...actions.map((a, i) => `${i + 1}. ${mark[a.status]} ${label[a.area]}: ${a.action}`),\n];\nreturn [{ json: { overall: f.overall, red: f.red, yellow: f.yellow, actions, markdown: lines.join('\\n'), facts: f } }];"
},
"position": [
440,
300
],
"id": "d8d05c5a-6a5d-5108-8983-3ee982fc0663"
},
{
"name": "Summarize",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"parameters": {
"promptType": "define",
"text": "=사실(JSON): {{ JSON.stringify($json.facts) }}",
"options": {
"systemMessage": "You write the morning operations summary for an IT helpdesk team lead. Korean, exactly 3 sentences: overall state, the most urgent area, what to do first. Use only numbers from the facts JSON (a ratio like 0.0588 may be written as 5.88%). Do not guess causes.",
"maxIterations": 1
}
},
"position": [
660,
300
],
"id": "eb2cf7af-1b3a-5ef8-a3c0-7fbd0c5fd155"
},
{
"name": "Summary 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": [
660,
520
],
"id": "a16dab74-014f-5d6e-9da6-ccc337283b08"
},
{
"name": "Check summary",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const r = $('Make brief').first().json;\nconst answer = String($json.output ?? '').trim();\nconst allowed = new Set();\nconst add = (v) => { for (const d of [0, 1, 2, 3, 4, 6]) allowed.add(Number(v.toFixed(d))); };\nconst walk = (v) => {\n if (typeof v === 'number') { add(v); add(v * 100); }\n else if (typeof v === 'string') { for (const m of v.match(/\\d+(?:\\.\\d+)?/g) ?? []) add(Number(m)); }\n else if (v && typeof v === 'object') { for (const [k, x] of Object.entries(v)) { walk(k); walk(x); } }\n};\nwalk(r.facts);\nconst versions = JSON.stringify(r.facts).match(/\\d+\\.\\d+\\.\\d+/g) ?? [];\nlet text = answer;\nfor (const v of versions) text = text.split(v).join(' ');\nconst unknown = (text.match(/\\d+(?:\\.\\d+)?/g) ?? []).filter((m) => !allowed.has(Number(m)));\nconst { facts, ...rest } = r;\nreturn [{ json: { ...rest, summary: answer, markdown: r.markdown + '\\n\\n## 요약\\n\\n' + answer, numbers_ok: unknown.length === 0, unknown_numbers: [...new Set(unknown)] } }];"
},
"position": [
880,
300
],
"id": "eaf526e2-064c-5d5c-ad13-67d237d99c4c"
},
{
"name": "Send brief",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"position": [
1100,
300
],
"id": "9c60667c-d76b-585b-b79d-462504312f77"
}
],
"connections": {
"Brief webhook": {
"main": [
[
{
"node": "Get facts",
"type": "main",
"index": 0
}
]
]
},
"Get facts": {
"main": [
[
{
"node": "Make brief",
"type": "main",
"index": 0
}
]
]
},
"Make brief": {
"main": [
[
{
"node": "Summarize",
"type": "main",
"index": 0
}
]
]
},
"Summary model": {
"ai_languageModel": [
[
{
"node": "Summarize",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Summarize": {
"main": [
[
{
"node": "Check summary",
"type": "main",
"index": 0
}
]
]
},
"Check summary": {
"main": [
[
{
"node": "Send brief",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}