{ "name": "75 IT helpdesk + long question cap (answer)", "nodes": [ { "name": "Question webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "parameters": { "httpMethod": "POST", "path": "my-cost", "responseMode": "responseNode", "options": {} }, "webhookId": "797db9f9-e9ff-5363-9d0a-58d23a2047f0", "position": [ 0, 300 ], "id": "3ccc3834-a583-51bd-a80f-b20124e32b64" }, { "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": "7e27dce9-fa3b-5c88-98b3-1dce956d16d2" }, { "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": "f3591be9-5676-5bb7-ab46-6984719af080" }, { "name": "Get budget", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "parameters": { "url": "http://data-api:8800/budget", "options": {} }, "position": [ 440, 300 ], "id": "4d62a650-0746-5e07-b811-2da4fdd28051" }, { "name": "Get prices", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "parameters": { "url": "http://data-api:8800/prices", "options": {} }, "position": [ 1100, 300 ], "id": "baa4796d-564b-5cc0-949a-d79738b12ba7" }, { "name": "Prepare question", "type": "n8n-nodes-base.code", "typeVersion": 2, "parameters": { "jsCode": "const q = String($('Question webhook').first().json.body.question ?? '');\nconst max = Number($('Get budget').first().json.max_question_chars ?? 500);\nif (q.length <= max) return [{ json: { question: q, trimmed: false, question_chars: q.length } }];\nconst head = Math.floor(max * 0.4);\nconst question = q.slice(0, head) + ' … ' + q.slice(q.length - (max - head));\nreturn [{ json: { question, trimmed: true, question_chars: q.length } }];" }, "position": [ 1320, 300 ], "id": "07d55a70-f59d-584f-bbae-3cfcf59973ac" }, { "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": "={{ $('Prepare question').first().json.question }}" } ] } }, "position": [ 1540, 300 ], "id": "e2005518-7a82-579a-acda-cc74b33968a9" }, { "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": "9d6c9b87-54bc-5433-bad3-6e33744e10ca" }, { "name": "Rule classify", "type": "n8n-nodes-base.code", "typeVersion": 2, "parameters": { "jsCode": "const q = String($('Prepare question').first().json.question ?? '');\nconst results = $('Search FAQ').first().json.results ?? [];\nconst category = /T-?\\d{4}/i.test(q) ? 'ticket' : (results.length ? 'faq' : 'unknown');\nreturn [{ json: { category } }];" }, "position": [ 2420, 300 ], "id": "ff9310ae-451e-5825-bb13-67013e490b85" }, { "name": "Known?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "conditions": [ { "id": "c1", "leftValue": "={{ $json.category }}", "rightValue": "unknown", "operator": { "type": "string", "operation": "notEquals" } } ], "combinator": "and" }, "options": {} }, "position": [ 2640, 300 ], "id": "14570236-0f99-5931-8173-d03f18288d68" }, { "name": "Classify", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "parameters": { "method": "POST", "url": "https://api.openai.com/v1/chat/completions", "authentication": "predefinedCredentialType", "nodeCredentialType": "openAiApi", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ model: 'gpt-5.4-nano', temperature: 0, messages: [{ role: 'system', content: \"You sort one company IT help-desk question. Categories: faq (how to fix or use something), ticket (asks about the status of a ticket like T-1002), other. Return only JSON, no code fence: {\\\"category\\\": \\\"faq\\\"}\" }, { role: 'user', content: $('Prepare question').first().json.question }] }) }}", "options": { "timeout": 60000 } }, "position": [ 2860, 520 ], "credentials": { "openAiApi": { "id": "openAiCourse0001", "name": "OpenAI (.env)" } }, "id": "07e553a6-f8eb-5513-a566-db99f5b11d29" }, { "name": "Parse category", "type": "n8n-nodes-base.code", "typeVersion": 2, "parameters": { "jsCode": "const raw = String($json.choices?.[0]?.message?.content ?? '').replace(/^```(json)?\\s*|```$/g, '');\nlet category = 'other';\ntry { category = JSON.parse(raw).category ?? 'other'; } catch (error) { category = 'other'; }\nreturn [{ json: { category } }];" }, "position": [ 3080, 520 ], "id": "1a1ea3bc-20b6-5218-b12e-9bafa39f3d8c" }, { "name": "Answer", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "parameters": { "method": "POST", "url": "https://api.openai.com/v1/chat/completions", "authentication": "predefinedCredentialType", "nodeCredentialType": "openAiApi", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ model: 'gpt-4.1-mini', temperature: 0, max_completion_tokens: 200, messages: [{ role: 'system', content: \"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. Use 합니다체.\" }, { role: 'user', content: '질문: ' + $('Prepare question').first().json.question + '\\nFAQ: ' + JSON.stringify(($('Search FAQ').first().json.results ?? []).map((f) => ({ id: f.id, title: f.title, answer: f.answer }))) + '\\n접수 건: ' + JSON.stringify((({ ticket_id, title, status, note, error }) => ({ ticket_id, title, status, note, error }))($('Get ticket').first().json)) }] }) }}", "options": { "timeout": 60000 } }, "position": [ 3300, 300 ], "credentials": { "openAiApi": { "id": "openAiCourse0001", "name": "OpenAI (.env)" } }, "id": "fd329729-e718-56ff-805c-aa298d26c0b2" }, { "name": "Build result", "type": "n8n-nodes-base.code", "typeVersion": 2, "parameters": { "jsCode": "const ran = (n) => { try { return $(n).isExecuted; } catch (e) { return false; } };\nconst steps = [{\"step\": \"classify\", \"node\": \"Classify\", \"model\": \"gpt-5.4-nano\"}, {\"step\": \"answer\", \"node\": \"Answer\", \"model\": \"gpt-4.1-mini\"}];\nconst prices = $('Get prices').first().json.models;\nconst by_step = {};\nlet prompt_tokens = 0, completion_tokens = 0, llm_calls = 0, cost_usd = 0;\nfor (const s of steps) {\n if (!ran(s.node)) continue;\n const u = $(s.node).first().json.usage ?? {};\n const row = { model: s.model, prompt_tokens: u.prompt_tokens ?? 0, completion_tokens: u.completion_tokens ?? 0 };\n const p = prices[s.model] ?? { input: 0, output: 0 };\n row.cost_usd = Math.round((row.prompt_tokens * p.input + row.completion_tokens * p.output) / 1e6 * 1e8) / 1e8;\n by_step[s.step] = row;\n prompt_tokens += row.prompt_tokens; completion_tokens += row.completion_tokens; cost_usd += row.cost_usd; llm_calls += 1;\n}\nconst results = $('Search FAQ').first().json.results ?? [];\nconst answer = String($('Answer').first().json.choices[0].message.content ?? '').trim();\nconst result = { route: '자동 답변', answer, sources: results.map((r) => r.id), llm_calls,\n usage: { prompt_tokens, completion_tokens, by_step }, cost_usd: Math.round(cost_usd * 1e8) / 1e8 };\nconst prepared = $('Prepare question').first().json;\nresult.trimmed = prepared.trimmed;\nresult.question_chars = prepared.question_chars;\nreturn [{ json: result }];" }, "position": [ 3520, 300 ], "id": "d85f4d2b-0957-5a81-a54b-35ba55509871" }, { "name": "Record usage", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "parameters": { "method": "POST", "url": "http://data-api:8800/usage", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ items: Object.entries($json.usage.by_step).map(([step, row]) => ({ request_id: $execution.id, user: $('Question webhook').first().json.body.user ?? '', step, model: row.model, prompt_tokens: row.prompt_tokens, completion_tokens: row.completion_tokens, cost_usd: row.cost_usd, route: $json.route })) }) }}", "options": { "response": { "response": { "neverError": true } }, "timeout": 3000 } }, "position": [ 3740, 300 ], "onError": "continueRegularOutput", "id": "a46b3503-5039-5cb6-890e-69149fe23f55" }, { "name": "Result again", "type": "n8n-nodes-base.code", "typeVersion": 2, "parameters": { "jsCode": "return [{ json: { ...$('Build result').first().json, recorded: Number($json.stored ?? 0) > 0 } }];" }, "position": [ 3960, 300 ], "id": "1b8127ae-a43b-5096-8f94-c96892a085db" }, { "name": "Send result", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, "parameters": { "respondWith": "json", "responseBody": "={{ $json }}", "options": {} }, "position": [ 4180, 300 ], "id": "6cbcbfe3-9306-5287-b0c6-79f3fe0f9a5e" } ], "connections": { "Question webhook": { "main": [ [ { "node": "Has question?", "type": "main", "index": 0 } ] ] }, "Has question?": { "main": [ [ { "node": "Get budget", "type": "main", "index": 0 } ], [ { "node": "Reject empty", "type": "main", "index": 0 } ] ] }, "Get budget": { "main": [ [ { "node": "Get prices", "type": "main", "index": 0 } ] ] }, "Get prices": { "main": [ [ { "node": "Prepare question", "type": "main", "index": 0 } ] ] }, "Prepare question": { "main": [ [ { "node": "Search FAQ", "type": "main", "index": 0 } ] ] }, "Search FAQ": { "main": [ [ { "node": "Get ticket", "type": "main", "index": 0 } ] ] }, "Get ticket": { "main": [ [ { "node": "Rule classify", "type": "main", "index": 0 } ] ] }, "Rule classify": { "main": [ [ { "node": "Known?", "type": "main", "index": 0 } ] ] }, "Known?": { "main": [ [ { "node": "Answer", "type": "main", "index": 0 } ], [ { "node": "Classify", "type": "main", "index": 0 } ] ] }, "Answer": { "main": [ [ { "node": "Build result", "type": "main", "index": 0 } ] ] }, "Build result": { "main": [ [ { "node": "Record usage", "type": "main", "index": 0 } ] ] }, "Record usage": { "main": [ [ { "node": "Result again", "type": "main", "index": 0 } ] ] }, "Result again": { "main": [ [ { "node": "Send result", "type": "main", "index": 0 } ] ] }, "Classify": { "main": [ [ { "node": "Parse category", "type": "main", "index": 0 } ] ] }, "Parse category": { "main": [ [ { "node": "Answer", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1" } }