Files
aio-01d4-stu/Last3Mon/72/answers/72-lab01-challenge2-budget.json
T

587 lines
15 KiB
JSON

{
"name": "72 IT helpdesk + time budget (answer)",
"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": "f7a423a6-a6a2-57a3-954f-e8540a1ce022"
},
{
"name": "Has question?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.body.question }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"position": [
220,
300
],
"id": "6dce76b0-2dd0-5181-99be-b0b894c87c8b"
},
{
"name": "Reject empty",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "{ \"error\": \"question is required\" }",
"options": {
"responseCode": 400
}
},
"position": [
440,
560
],
"id": "75371917-2483-583e-9135-5c663a18b08b"
},
{
"name": "Start clock",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { t: Date.now() } }];"
},
"position": [
440,
300
],
"id": "34377f95-81c5-5aa3-9e2c-6b63a8e5ae54"
},
{
"name": "Classify",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"parameters": {
"promptType": "define",
"text": "={{ $('Question webhook').first().json.body.question }}",
"options": {
"systemMessage": "You sort one company IT help-desk question. Categories: faq (how to fix or use something: password, account lock, VPN, printer, mail box, software install), ticket (asks about the status of an IT request ticket like T-1002), other (anything else). Return only JSON, no code fence: {\"category\": \"faq\"}",
"maxIterations": 1
}
},
"position": [
660,
300
],
"id": "7a93b236-dbff-52c5-9397-61087bc0688d"
},
{
"name": "Classify 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": "357e7611-4597-5ba3-a6ba-9a4d4d63cda7"
},
{
"name": "Parse category",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const raw = String($json.output ?? '').trim().replace(/^```(json)?\\s*|```$/g, '');\nlet category = 'other';\ntry { category = JSON.parse(raw).category ?? 'other'; } catch (error) { category = 'other'; }\nif (!['faq', 'ticket', 'other'].includes(category)) category = 'other';\nreturn [{ json: { category, by: 'llm' } }];"
},
"position": [
880,
300
],
"id": "9327634c-6764-5403-b7bc-3e666e7ae3d6"
},
{
"name": "After classify",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
1100,
300
],
"id": "8f3da4f6-ce34-5505-b543-e5d68d990a96"
},
{
"name": "Search FAQ",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/faq",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "q",
"value": "={{ $('Question webhook').first().json.body.question }}"
}
]
}
},
"position": [
1320,
300
],
"id": "103c6254-01ea-5e80-81bb-7e3b9ea7beb5"
},
{
"name": "After FAQ",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
1540,
300
],
"id": "d03181b0-715d-5fae-98a0-92c52a2b3b3c"
},
{
"name": "Get ticket",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "=http://data-api:8800/tickets/{{ (($('Question webhook').first().json.body.question.match(/T-?\\d{4}/i) || ['NONE'])[0]).toUpperCase().replace(/^T(\\d)/, 'T-$1') }}",
"options": {
"response": {
"response": {
"neverError": true
}
}
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "requester",
"value": "={{ $('Question webhook').first().json.body.user ?? '' }}"
}
]
}
},
"position": [
1760,
300
],
"id": "f20b5c78-0224-5d55-8d77-0a9e98061a6b"
},
{
"name": "After data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
1980,
300
],
"id": "cca89aef-ef17-5b5f-a3c3-a53e0d762b43"
},
{
"name": "Answer",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"parameters": {
"promptType": "define",
"text": "=질문: {{ $('Question webhook').first().json.body.question }}\n분류: {{ $('Parse category').first().json.category }}\nFAQ: {{ JSON.stringify($('Search FAQ').first().json.results ?? []) }}\n접수 건: {{ JSON.stringify($('Get ticket').first().json) }}",
"options": {
"systemMessage": "You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say \"찾지 못했습니다\" and that a staff member will help.",
"maxIterations": 1
}
},
"position": [
2200,
300
],
"id": "d7ec1c62-265f-5d16-b67a-c5922305bb8a"
},
{
"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": [
2200,
520
],
"id": "5c7f8b81-cd4c-59a2-9899-9bdb4813f7ab"
},
{
"name": "After answer",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
2420,
300
],
"id": "43dab328-4901-5fd6-8238-6e0f836cc31d"
},
{
"name": "Review",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"parameters": {
"promptType": "define",
"text": "=초안: {{ $('Answer').first().json.output }}",
"options": {
"systemMessage": "You review a Korean IT help-desk answer before it is sent. Keep the facts exactly. Make it polite (합니다체), remove anything not supported by the data, keep at most 3 sentences. Return only the final answer text.",
"maxIterations": 1
}
},
"position": [
2640,
300
],
"id": "a4583946-f582-5ad8-963b-b866b74e9ba4"
},
{
"name": "Review 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": [
2640,
520
],
"id": "3be9acc9-4b3d-5a41-b6a4-00fa1b04d777"
},
{
"name": "After review",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$json, t: Date.now() } }];"
},
"position": [
2860,
300
],
"id": "d07d9992-7e5e-5aab-b925-9387021bbaff"
},
{
"name": "Build result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const ran = (name) => { try { return $(name).isExecuted; } catch (error) { return false; } };\nconst body = $('Question webhook').first().json.body;\nconst category = $('Parse category').first().json.category;\nconst answer = String($('Review').first().json.output ?? '').trim();\nconst results = $('Search FAQ').first().json.results ?? [];\nconst sources = category === 'faq' ? results.map((r) => r.id) : [];\nconst steps = [];\nif (ran('Rule classify')) steps.push('rule');\nif (ran('Classify')) steps.push('classify');\nsteps.push('faq', 'ticket', 'answer');\nsteps.push('review');\nconst llm_calls = (ran('Classify') ? 1 : 0) + 1 + 1;\nconst result = { route: '자동 답변', category, answer, sources };\nconst t = (name) => (ran(name) ? $(name).first().json.t : null);\nconst t0 = t('Start clock');\nconst marks = [['classify', t('After classify')], ['faq', t('After FAQ')], ['ticket', t('After data')], ['answer', t('After answer')], ['review', t('After review')]];\nconst timings = {};\nlet last = t0;\nfor (const [label, mark] of marks) {\n if (mark === null) continue;\n timings[`${label}_ms`] = mark - last;\n last = mark;\n}\ntimings.total_ms = Date.now() - t0;\nObject.assign(result, { llm_calls, cached: false, steps, timings });\nconst parts = Object.entries(timings).filter(([key]) => key !== 'total_ms');\nresult.slowest_step = parts.sort((a, b) => b[1] - a[1])[0][0].replace(/_ms$/, '');\nconst budget_ms = Number(body.budget_ms ?? 3000);\nresult.llm_ms = ['classify', 'answer', 'review'].reduce((sum, label) => sum + (timings[`${label}_ms`] ?? 0), 0);\nresult.budget_ms = budget_ms;\nresult.over_budget = timings.total_ms > budget_ms;\nreturn [{ json: result }];"
},
"position": [
3080,
300
],
"id": "079e44f0-da3e-57dd-be6a-87025b6a320f"
},
{
"name": "Send result",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"position": [
3300,
300
],
"id": "8ac7b99c-d9fa-5a1e-a168-33eb7ab5e2a3"
}
],
"connections": {
"Question webhook": {
"main": [
[
{
"node": "Has question?",
"type": "main",
"index": 0
}
]
]
},
"Has question?": {
"main": [
[
{
"node": "Start clock",
"type": "main",
"index": 0
}
],
[
{
"node": "Reject empty",
"type": "main",
"index": 0
}
]
]
},
"Start clock": {
"main": [
[
{
"node": "Classify",
"type": "main",
"index": 0
}
]
]
},
"Classify model": {
"ai_languageModel": [
[
{
"node": "Classify",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Classify": {
"main": [
[
{
"node": "Parse category",
"type": "main",
"index": 0
}
]
]
},
"Parse category": {
"main": [
[
{
"node": "After classify",
"type": "main",
"index": 0
}
]
]
},
"After classify": {
"main": [
[
{
"node": "Search FAQ",
"type": "main",
"index": 0
}
]
]
},
"Search FAQ": {
"main": [
[
{
"node": "After FAQ",
"type": "main",
"index": 0
}
]
]
},
"After FAQ": {
"main": [
[
{
"node": "Get ticket",
"type": "main",
"index": 0
}
]
]
},
"Get ticket": {
"main": [
[
{
"node": "After data",
"type": "main",
"index": 0
}
]
]
},
"After data": {
"main": [
[
{
"node": "Answer",
"type": "main",
"index": 0
}
]
]
},
"Answer model": {
"ai_languageModel": [
[
{
"node": "Answer",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Answer": {
"main": [
[
{
"node": "After answer",
"type": "main",
"index": 0
}
]
]
},
"After answer": {
"main": [
[
{
"node": "Review",
"type": "main",
"index": 0
}
]
]
},
"Review model": {
"ai_languageModel": [
[
{
"node": "Review",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Review": {
"main": [
[
{
"node": "After review",
"type": "main",
"index": 0
}
]
]
},
"After review": {
"main": [
[
{
"node": "Build result",
"type": "main",
"index": 0
}
]
]
},
"Build result": {
"main": [
[
{
"node": "Send result",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}