424 lines
18 KiB
JSON
424 lines
18 KiB
JSON
{
|
|
"name": "75 IT helpdesk + safe record (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 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": "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": [
|
|
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": "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-4.1-mini', 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: $('Question webhook').first().json.body.question }] }) }}",
|
|
"options": {
|
|
"timeout": 60000
|
|
}
|
|
},
|
|
"credentials": {
|
|
"openAiApi": {
|
|
"id": "openAiCourse0001",
|
|
"name": "OpenAI (.env)"
|
|
}
|
|
},
|
|
"position": [
|
|
2420,
|
|
300
|
|
],
|
|
"id": "07e553a6-f8eb-5513-a566-db99f5b11d29"
|
|
},
|
|
{
|
|
"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, 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 합니다체.\\n\\n참고 자료 (사내 규정 전체):\\n# 업무용 장비 지급·반납 규정\\n\\n## 1. 지급과 교체\\n- 업무용 노트북은 입사일에 지급한다.\\n- 노트북 교체 주기는 4년이다. 고장이나 배터리 교체는 IT 문의로 접수한다.\\n\\n## 2. 분실과 도난\\n- 업무용 장비를 잃어버리거나 도난당하면 24시간 이내에 IT 문의와 보안팀에 신고한다.\\n- 신고가 접수되면 IT 담당자가 원격으로 장비의 데이터를 잠근다.\\n\\n## 3. 반납\\n- 퇴사하거나 장비를 교체할 때는 5영업일 이내에 장비를 IT 담당자에게 반납한다.\\n- 반납 전에 개인 파일을 지우지 말고, 업무 파일은 사내 드라이브로 옮긴다.\\n\\n## 4. 개인 장비 사용\\n- 개인 휴대폰이나 태블릿으로는 업무 메일과 메신저만 쓸 수 있다.\\n- 개인 장비로 업무 메일을 보려면 회사의 모바일 관리 앱(MDM)을 먼저 설치한다.\\n\\n# 메일·문서 보안 규정\\n\\n## 1. 메일함\\n- 개인 메일함 용량은 10GB이다. 용량이 90%를 넘으면 안내 메일이 발송된다.\\n- 오래된 메일은 보관함으로 옮겨 용량을 확보한다.\\n\\n## 2. 첨부 파일\\n- 메일 첨부 파일은 25MB까지 보낼 수 있다.\\n- 25MB를 넘는 파일은 사내 드라이브에 올리고 공유 링크를 보낸다.\\n- 개인정보가 들어 있는 파일을 외부로 보낼 때는 반드시 암호를 걸고, 암호는 메신저 등 다른 경로로 알려 준다.\\n\\n## 3. 문서 등급과 보관\\n- 대외비 문서에는 문서 첫 장에 '대외비'라고 표시한다.\\n- 업무 문서는 3년 동안 보관한 뒤 정리한다.\\n\\n# 회의실 이용 규정\\n\\n## 1. 예약\\n- 회의실은 사내 포털에서 예약한다. 한 번에 최대 3시간까지 예약할 수 있다.\\n- 예약 시작 후 10분이 지나도록 아무도 사용하지 않으면 예약이 자동으로 취소된다.\\n\\n## 2. 장비\\n- 대회의실의 화상회의 장비를 쓰려면 회의 시작 30분 전까지 IT 문의로 요청한다.\\n- 프로젝터는 B회의실과 대회의실에 있다.\\n\\n## 3. 사용 후\\n- 회의가 끝나면 화이트보드를 지우고 의자를 정리한다.\\n\\n# 비밀번호·계정 보안 규정\\n\\n## 1. 비밀번호 만들기\\n- 비밀번호는 12자 이상으로 만든다.\\n- 영문 대문자, 영문 소문자, 숫자, 특수문자 중 3종류 이상을 섞는다.\\n- 이름, 생일, 전화번호, 회사 이름처럼 추측하기 쉬운 단어는 쓰지 않는다.\\n\\n## 2. 변경 주기\\n- 비밀번호는 90일마다 바꾼다. 만료 7일 전부터 사내 포털에 변경 안내가 표시된다.\\n- 최근에 쓴 비밀번호 5개는 다시 쓸 수 없다.\\n\\n## 3. 로그인 실패와 잠금\\n- 로그인에 5번 연속 실패하면 계정이 30분 동안 잠긴다.\\n- 30분이 지나면 자동으로 풀린다. 급한 경우 IT 문의로 잠금 해제를 요청한다.\\n\\n## 4. 다단계 인증\\n- 회사 밖에서 사내 시스템에 접속할 때는 OTP 다단계 인증을 반드시 사용한다.\\n- OTP 앱이 설치된 휴대폰을 바꾸면 IT 포털에서 OTP를 다시 등록한다.\\n\\n# 개인정보 처리 지침\\n\\n## 1. 수집\\n- 업무에 꼭 필요한 개인정보만 수집한다.\\n- 주민등록번호는 법에서 정한 경우가 아니면 수집하지 않는다.\\n\\n## 2. 이용과 보관\\n- 고객 개인정보가 보이는 화면은 캡처하거나 촬영하지 않는다.\\n- 개인정보가 들어 있는 출력물은 다 쓴 뒤 바로 파쇄한다.\\n\\n## 3. 유출 대응\\n- 개인정보가 유출되었거나 유출이 의심되면 1시간 이내에 보안팀에 신고한다.\\n- 신고할 때는 유출된 정보의 종류, 대략적인 건수, 발견한 시각을 함께 알린다.\\n\\n# 원격근무·VPN 이용 규정\\n\\n## 1. 접속 원칙\\n- 회사 밖에서 사내 시스템에 접속할 때는 반드시 회사 VPN을 먼저 연결한다.\\n- 카페, 공항 같은 공용 와이파이에서는 VPN 없이 사내 시스템에 접속하지 않는다.\\n\\n## 2. VPN 인증서\\n- VPN 인증서의 유효 기간은 1년이다.\\n- 만료 30일 전부터 IT 포털의 '인증서 재발급' 메뉴에서 새 인증서를 신청한다.\\n- 인증서가 만료되면 VPN에 접속할 수 없으므로, 만료 전에 재발급한다.\\n\\n## 3. 재택근무 중 보안\\n- 자리를 비울 때는 화면을 잠그고, 5분 동안 사용하지 않으면 자동으로 잠기게 설정한다.\\n- 가족과 함께 쓰는 컴퓨터로는 업무를 하지 않는다.\\n- VPN 접속 기록은 1년 동안 보관된다.\\n\\n# 보안 사고 대응 절차\\n\\n## 1. 피싱 메일\\n- 보낸 사람이 의심스럽거나 급하게 클릭을 요구하는 메일은 링크를 누르지 않는다.\\n- 메일 화면의 '피싱 신고' 버튼으로 신고한다.\\n- 이미 링크를 눌렀거나 비밀번호를 입력했다면 즉시 비밀번호를 바꾸고 IT 문의로 알린다.\\n\\n## 2. 악성코드 감염 의심\\n- 컴퓨터가 갑자기 느려지거나 모르는 프로그램이 실행되면 악성코드 감염을 의심한다.\\n- 감염이 의심되면 먼저 랜선을 뽑고 와이파이를 꺼서 네트워크에서 분리한 뒤 IT 문의로 알린다.\\n- 컴퓨터를 끄거나 파일을 지우지 않는다. 원인 분석에 필요하다.\\n\\n# 소프트웨어 사용 규정\\n\\n## 1. 설치와 승인\\n- 업무용 컴퓨터에는 승인된 소프트웨어만 설치한다.\\n- 새 소프트웨어가 필요하면 IT 포털의 '소프트웨어 신청'에서 요청한다. 승인까지 보통 2영업일이 걸린다.\\n- 무료 소프트웨어라도 회사 업무에 쓰려면 상업적 이용이 허용되는지 라이선스를 확인한다.\\n\\n## 2. 금지 사항\\n- 불법으로 복제한 소프트웨어는 설치하지 않는다. 적발되면 징계 대상이 된다.\\n\\n## 3. 생성형 AI 서비스\\n- 회사가 승인한 생성형 AI 서비스만 업무에 사용한다.\\n- 고객 정보, 개인정보, 대외비 문서는 생성형 AI 서비스에 입력하지 않는다.\\n- 생성형 AI가 만든 결과는 사람이 확인한 뒤에 사용한다.\" }, { role: 'user', content: '질문: ' + $('Question webhook').first().json.body.question + '\\nFAQ: ' + JSON.stringify($('Search FAQ').first().json.results ?? []) + '\\n접수 건: ' + JSON.stringify($('Get ticket').first().json) }] }) }}",
|
|
"options": {
|
|
"timeout": 60000
|
|
}
|
|
},
|
|
"credentials": {
|
|
"openAiApi": {
|
|
"id": "openAiCourse0001",
|
|
"name": "OpenAI (.env)"
|
|
}
|
|
},
|
|
"position": [
|
|
2640,
|
|
300
|
|
],
|
|
"id": "fd329729-e718-56ff-805c-aa298d26c0b2"
|
|
},
|
|
{
|
|
"name": "Review",
|
|
"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, messages: [{ role: 'system', content: \"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.\" }, { role: 'user', content: '초안: ' + $('Answer').first().json.choices[0].message.content }] }) }}",
|
|
"options": {
|
|
"timeout": 60000
|
|
}
|
|
},
|
|
"credentials": {
|
|
"openAiApi": {
|
|
"id": "openAiCourse0001",
|
|
"name": "OpenAI (.env)"
|
|
}
|
|
},
|
|
"position": [
|
|
2860,
|
|
300
|
|
],
|
|
"id": "f289c264-5e1f-523d-8e18-67ba4cbb166c"
|
|
},
|
|
{
|
|
"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-4.1-mini\"}, {\"step\": \"answer\", \"node\": \"Answer\", \"model\": \"gpt-4.1-mini\"}, {\"step\": \"review\", \"node\": \"Review\", \"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($('Review').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 };\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 prices",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Reject empty",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Get prices": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Search FAQ",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Search FAQ": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Get ticket",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Get ticket": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Classify",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Classify": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Answer",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Review",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Review": {
|
|
"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
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"settings": {
|
|
"executionOrder": "v1"
|
|
}
|
|
}
|