65일차 실습자료

This commit is contained in:
mcchae committed 2026-10-13 17:09:59 +09:00
1 parent d6a6c62a3f
commit 152344cb13
42 files changed
+4545

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
# Compose settings for the day 65 n8n + Ollama (fallback) containers. No API key here:
# the OpenAI key goes in Last3Mon/.env, and register-credentials.sh puts it into n8n.
# Copy to .env and keep the line that matches how Ollama was installed on day 64.
# Ollama installed with the install script (runs as a service):
OLLAMA_DATA=/usr/share/ollama/.ollama
# Ollama started by hand with `ollama serve`:
# OLLAMA_DATA=/home/<your-wsl-user>/.ollama
+35
View File
@@ -0,0 +1,35 @@
# n8n (LLM: OpenAI API, key from Last3Mon/.env via register-credentials.sh) + Ollama as the fallback.
# docker compose up -d (run from this folder)
# n8n editor : http://localhost:5678
# Ollama API : http://localhost:11434 (n8n uses http://ollama:11434 inside the network)
name: day65-n8n
services:
n8n:
image: docker.n8n.io/n8nio/n8n:2.42.6
ports:
- "5678:5678"
environment:
- GENERIC_TIMEZONE=Asia/Seoul
- TZ=Asia/Seoul
- N8N_SECURE_COOKIE=false
volumes:
- n8n-data:/home/node/.n8n
- ./workflows:/home/node/workflows:ro
depends_on:
- ollama
restart: unless-stopped
ollama:
image: ollama/ollama:latest
ports:
- "11434:11434"
volumes:
# Reuse the models downloaded on day 64 by setting OLLAMA_DATA in .env
# (default: a new Docker volume, then run: docker compose exec ollama ollama pull gemma3:4b)
- ${OLLAMA_DATA:-ollama-data}:/root/.ollama
restart: unless-stopped
volumes:
n8n-data:
ollama-data:
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Register the course credentials in n8n from the .env file, so the API key is never typed into
# a workflow or committed to git.
# cd Last3Mon/65/n8n && docker compose up -d && bash register-credentials.sh
#
# Creates (or updates) two credentials with fixed ids that the course workflows already use:
# OpenAI (.env) id openAiCourse0001 <- OPENAI_API_KEY (and OPENAI_BASE_URL if set)
# Ollama local id ollamaLocal0001 <- http://ollama:11434 (fallback model)
#
# Outage drill (day 72): OPENAI_API_KEY=sk-invalid-key-for-drill-000000 bash register-credentials.sh
# Restore: bash register-credentials.sh
set -euo pipefail
cd "$(dirname "$0")"
# The nearest .env (this folder first, then the folders above) that has OPENAI_API_KEY.
env_file=""
dir="$PWD"
while [ "$dir" != "/" ]; do
if [ -f "$dir/.env" ] && grep -q '^OPENAI_API_KEY=' "$dir/.env"; then
env_file="$dir/.env"
break
fi
dir="$(dirname "$dir")"
done
if [ -z "$env_file" ]; then
echo "No .env with OPENAI_API_KEY found. Copy Last3Mon/.env.example to Last3Mon/.env and fill it in." >&2
exit 1
fi
value() { { grep -E "^$1=" "$env_file" || true; } | tail -1 | cut -d= -f2- | sed -e 's/^["'\'']//' -e 's/["'\'']$//'; }
key="${OPENAI_API_KEY:-$(value OPENAI_API_KEY)}" # a key exported in the shell wins (used for outage drills)
base_url="$(value OPENAI_BASE_URL)"
base_url="${base_url:-https://api.openai.com/v1}"
if ! printf '%s' "$key" | grep -Eq '^[A-Za-z0-9_-]{20,}$'; then
echo "OPENAI_API_KEY in $env_file is empty or does not look like an API key." >&2
exit 1
fi
credentials=$(printf '[{"id":"openAiCourse0001","name":"OpenAI (.env)","type":"openAiApi","data":{"apiKey":"%s","url":"%s"}},{"id":"ollamaLocal0001","name":"Ollama local","type":"ollamaApi","data":{"baseUrl":"http://ollama:11434"}}]' "$key" "$base_url")
# The JSON goes through stdin into a temporary file inside the container, then is deleted.
printf '%s' "$credentials" | docker compose exec -T n8n sh -c \
'cat > /tmp/course-credentials.json && n8n import:credentials --input=/tmp/course-credentials.json; status=$?; rm -f /tmp/course-credentials.json; exit $status'
echo "Registered 'OpenAI (.env)' and 'Ollama local' from $env_file"
@@ -0,0 +1,170 @@
{
"name": "65-01 Condition basics",
"nodes": [
{
"parameters": {},
"id": "26dc330d-6170-5f39-aeb4-d1bc63ed9d26",
"name": "Start",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
0,
0
]
},
{
"parameters": {
"jsCode": "// Three sample requests. In a real workflow they would come from a form, mail or webhook.\nreturn [\n { json: { id: 'T-1', text: 'VPN이 끊겨서 고객 미팅에 들어갈 수 없어요', priority: 'urgent' } },\n { json: { id: 'T-2', text: '프린터 토너 교체를 요청합니다', priority: 'normal' } },\n { json: { id: 'T-3', text: '계정이 잠겨서 결재를 올릴 수 없어요', priority: 'urgent' } },\n];"
},
"id": "636773ca-d229-564c-88cb-54b530628910",
"name": "Sample requests",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
220,
0
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "4db4fdb9-dbaf-51bb-aaf9-16a54ef93027",
"leftValue": "={{ $json.priority }}",
"operator": {
"type": "string",
"operation": "equals"
},
"rightValue": "urgent"
}
],
"combinator": "and"
},
"options": {}
},
"id": "e54594bf-eb8b-5d83-96db-ef857683c127",
"name": "Is urgent?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
440,
0
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "1f7b6098-3185-56cc-9d8e-4bcfec1e7ba4",
"name": "route",
"value": "즉시 담당자 연결",
"type": "string"
},
{
"id": "c01612ed-a8a0-5a0a-83b9-93f6ad2eaf31",
"name": "sla_minutes",
"value": "30",
"type": "number"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "1d7d3d65-295f-599e-909f-8d5708d8ff46",
"name": "Urgent route",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
680,
-100
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "e7c65f93-009f-5a5a-8cad-71abcc7cc569",
"name": "route",
"value": "일반 접수",
"type": "string"
},
{
"id": "7148df87-83e5-54ca-9d67-619c60e6fe54",
"name": "sla_minutes",
"value": "480",
"type": "number"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "9b40293c-c2f0-55a6-b1ba-232770d31ee9",
"name": "Normal route",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
680,
100
]
}
],
"connections": {
"Start": {
"main": [
[
{
"node": "Sample requests",
"type": "main",
"index": 0
}
]
]
},
"Sample requests": {
"main": [
[
{
"node": "Is urgent?",
"type": "main",
"index": 0
}
]
]
},
"Is urgent?": {
"main": [
[
{
"node": "Urgent route",
"type": "main",
"index": 0
}
],
[
{
"node": "Normal route",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"pinData": {},
"id": "aeb87743007e5e"
}
@@ -0,0 +1,479 @@
{
"name": "65-02 Webhook request router",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "request-route",
"responseMode": "responseNode",
"options": {}
},
"id": "8dc39098-cbf2-5da7-b6a3-0541c2838c7d",
"name": "Request webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
0
],
"webhookId": "612ab961-6b30-5c73-a4b0-b113262a92e8"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "0e03d4eb-275c-5799-8c08-3e744e4692e2",
"leftValue": "={{ $json.body.text }}",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"rightValue": ""
}
],
"combinator": "and"
},
"options": {}
},
"id": "d19bdfca-d55e-5760-a488-458abc7a5480",
"name": "Has text?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
220,
0
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { \"error\": \"text is required\" } }}",
"options": {
"responseCode": 400
}
},
"id": "5adfc553-1a2c-55fd-a4bd-d6564197222a",
"name": "Reject empty",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
460,
160
]
},
{
"parameters": {
"jsCode": "// Rule-based classification (no AI yet): the first rule whose keyword appears wins.\nconst rules = [\n { category: 'account', words: ['비밀번호', '계정', '로그인', '잠겨', '잠김'] },\n { category: 'network', words: ['vpn', '인터넷', '와이파이', '접속', '네트워크'] },\n { category: 'device', words: ['프린터', '모니터', '키보드', '마우스', '노트북'] },\n { category: 'software', words: ['설치', '프로그램', '라이선스', '메일', '업데이트'] },\n];\nconst body = $input.first().json.body;\nconst text = String(body.text).toLowerCase();\nconst hit = rules.find(r => r.words.some(w => text.includes(w)));\nreturn [{ json: { user: body.user || 'anonymous', text: body.text, category: hit ? hit.category : 'other' } }];"
},
"id": "555904e3-48b4-5e74-bbe5-11a9f9153504",
"name": "Keyword classify",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
460,
-40
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "befaac8f-1c7b-5a01-afb6-1dfca95d48c5",
"leftValue": "={{ $json.category }}",
"operator": {
"type": "string",
"operation": "equals"
},
"rightValue": "account"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "account"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "fd212d44-e661-5050-b426-360a5d39ad96",
"leftValue": "={{ $json.category }}",
"operator": {
"type": "string",
"operation": "equals"
},
"rightValue": "network"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "network"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "eff4a459-e7e8-5c5a-84f1-3670a5bd03c6",
"leftValue": "={{ $json.category }}",
"operator": {
"type": "string",
"operation": "equals"
},
"rightValue": "device"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "device"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "862c0a38-3a85-5499-99f8-f0ffee9a6825",
"leftValue": "={{ $json.category }}",
"operator": {
"type": "string",
"operation": "equals"
},
"rightValue": "software"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "software"
}
]
},
"options": {
"fallbackOutput": "extra",
"renameFallbackOutput": "other"
}
},
"id": "2c251693-82e5-56cf-9a73-ed6bc2eebdf4",
"name": "Route by category",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
680,
-40
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "241fe967-6e70-51db-b65d-a59655f888b0",
"name": "team",
"value": "계정 관리 담당",
"type": "string"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "65675630-6d1a-55f5-adaa-b40125e2d453",
"name": "Team account",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
920,
-240
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "91e77bc6-3674-5f2a-a997-d831997f5a28",
"name": "team",
"value": "네트워크 담당",
"type": "string"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "0b91914c-9289-5f56-adc2-6d697240358c",
"name": "Team network",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
920,
-130
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "3fdfa927-2342-5f8a-95c2-bc5a1e0b87d9",
"name": "team",
"value": "장비 지원 담당",
"type": "string"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "0b918a6f-2ed5-5007-90f8-cc7911628ce7",
"name": "Team device",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
920,
-20
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "251f69b0-7fa1-52d0-b73f-4a3f102fff35",
"name": "team",
"value": "소프트웨어 담당",
"type": "string"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "ac9cabd3-3a8a-5055-af0a-3ad481eba531",
"name": "Team software",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
920,
90
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "7602acd9-08cd-500a-acbd-bd4656159712",
"name": "team",
"value": "IT 문의 접수 창구",
"type": "string"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "71c63724-4e06-5a26-bec9-ff4b97f9328c",
"name": "Team other",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
920,
200
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"id": "365b7747-c4de-5b2e-9dcc-3dc3f3d2d9ac",
"name": "Send route",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1160,
-40
]
}
],
"connections": {
"Request webhook": {
"main": [
[
{
"node": "Has text?",
"type": "main",
"index": 0
}
]
]
},
"Has text?": {
"main": [
[
{
"node": "Keyword classify",
"type": "main",
"index": 0
}
],
[
{
"node": "Reject empty",
"type": "main",
"index": 0
}
]
]
},
"Keyword classify": {
"main": [
[
{
"node": "Route by category",
"type": "main",
"index": 0
}
]
]
},
"Route by category": {
"main": [
[
{
"node": "Team account",
"type": "main",
"index": 0
}
],
[
{
"node": "Team network",
"type": "main",
"index": 0
}
],
[
{
"node": "Team device",
"type": "main",
"index": 0
}
],
[
{
"node": "Team software",
"type": "main",
"index": 0
}
],
[
{
"node": "Team other",
"type": "main",
"index": 0
}
]
]
},
"Team account": {
"main": [
[
{
"node": "Send route",
"type": "main",
"index": 0
}
]
]
},
"Team network": {
"main": [
[
{
"node": "Send route",
"type": "main",
"index": 0
}
]
]
},
"Team device": {
"main": [
[
{
"node": "Send route",
"type": "main",
"index": 0
}
]
]
},
"Team software": {
"main": [
[
{
"node": "Send route",
"type": "main",
"index": 0
}
]
]
},
"Team other": {
"main": [
[
{
"node": "Send route",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"pinData": {},
"id": "af8dc0f7ef905c"
}
@@ -0,0 +1,438 @@
{
"name": "65-03 AI request flow",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "ai-request",
"responseMode": "responseNode",
"options": {}
},
"id": "07b51032-62c2-549b-8cfb-e3f16a851ed7",
"name": "AI request webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
0
],
"webhookId": "26aa9aad-522f-5f3d-8b58-ae0d7c18ce66"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "cf988cf6-5aec-59ef-887c-93252d7d366b",
"name": "llm_url",
"value": "https://api.openai.com/v1",
"type": "string"
},
{
"id": "6d229077-7077-5e85-8ebb-8f6046227539",
"name": "model",
"value": "gpt-4.1-mini",
"type": "string"
},
{
"id": "8a827ce4-4716-5aad-ad39-9c105afcf895",
"name": "user",
"value": "={{ $json.body.user || 'anonymous' }}",
"type": "string"
},
{
"id": "462379ca-3e96-599d-9a1c-64bae0b1095a",
"name": "text",
"value": "={{ $json.body.text }}",
"type": "string"
}
]
},
"includeOtherFields": false,
"options": {}
},
"id": "6c57d7cf-acd9-5007-b34a-b741afa2c060",
"name": "Settings",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
220,
0
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "f2c175d2-9545-570a-99a6-e7cdfb17162f",
"leftValue": "={{ $json.text }}",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"rightValue": ""
}
],
"combinator": "and"
},
"options": {}
},
"id": "d19bdfca-d55e-5760-a488-458abc7a5480",
"name": "Has text?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
440,
0
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { \"error\": \"text is required\" } }}",
"options": {
"responseCode": 400
}
},
"id": "5adfc553-1a2c-55fd-a4bd-d6564197222a",
"name": "Reject empty",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
680,
180
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $('Settings').first().json.llm_url }}/chat/completions",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: $('Settings').first().json.model, temperature: 0, messages: [ { role: 'system', content: \"Classify the IT request into exactly one of: account, network, device, software, other. Reply with the label only.\" }, { role: 'user', content: $('Settings').first().json.text } ] }) }}",
"options": {
"timeout": 180000
},
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi"
},
"id": "d084b606-aad9-5341-98cd-490070f907ac",
"name": "LLM classify",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
680,
-40
],
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
}
},
{
"parameters": {
"jsCode": "// 1) Clean up the LLM label 2) Search the FAQ list\nconst allowed = ['account', 'network', 'device', 'software', 'other'];\nconst raw = String($input.first().json.choices?.[0]?.message?.content ?? '').trim().toLowerCase();\nconst category = allowed.find(a => raw.includes(a)) ?? 'other';\n\nconst faqs = [\n {\n \"id\": \"FAQ-001\",\n \"title\": \"비밀번호 재설정\",\n \"keywords\": [\n \"비밀번호\",\n \"패스워드\",\n \"password\",\n \"재설정\",\n \"초기화\"\n ],\n \"answer\": \"사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정합니다.\"\n },\n {\n \"id\": \"FAQ-002\",\n \"title\": \"계정 잠김 해제\",\n \"keywords\": [\n \"계정\",\n \"잠김\",\n \"잠금\",\n \"로그인\",\n \"실패\"\n ],\n \"answer\": \"로그인 5회 실패 시 30분 동안 잠깁니다. 30분 후 다시 시도하거나 헬프데스크에 잠금 해제를 요청합니다.\"\n },\n {\n \"id\": \"FAQ-003\",\n \"title\": \"VPN 접속 오류\",\n \"keywords\": [\n \"vpn\",\n \"원격\",\n \"재택\",\n \"접속\",\n \"연결\"\n ],\n \"answer\": \"VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다.\"\n },\n {\n \"id\": \"FAQ-004\",\n \"title\": \"프린터 출력 안 됨\",\n \"keywords\": [\n \"프린터\",\n \"출력\",\n \"인쇄\",\n \"용지\"\n ],\n \"answer\": \"프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력합니다.\"\n },\n {\n \"id\": \"FAQ-005\",\n \"title\": \"메일함 용량 초과\",\n \"keywords\": [\n \"메일\",\n \"용량\",\n \"메일함\",\n \"첨부\"\n ],\n \"answer\": \"오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다.\"\n },\n {\n \"id\": \"FAQ-006\",\n \"title\": \"소프트웨어 설치 요청\",\n \"keywords\": [\n \"설치\",\n \"소프트웨어\",\n \"프로그램\",\n \"라이선스\"\n ],\n \"answer\": \"IT 포털의 '소프트웨어 신청' 메뉴에서 승인 요청을 올리면 승인 후 원격으로 설치됩니다.\"\n }\n];\nconst settings = $('Settings').first().json;\nconst words = String(settings.text).toLowerCase().split(/\\s+/).filter(w => w.length > 1);\nconst scored = faqs\n .map(f => ({ ...f, score: words.filter(w => f.keywords.some(k => w.includes(k) || k.includes(w))).length }))\n .filter(f => f.score > 0)\n .sort((a, b) => b.score - a.score);\nconst faq = scored[0] ?? null;\n\nreturn [{ json: { user: settings.user, text: settings.text, raw_label: raw, category, found: faq !== null, faq } }];"
},
"id": "aa4be834-45fb-563b-95f5-c6a0dbeb2903",
"name": "Normalize and search FAQ",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
920,
-40
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "6a4b578f-d9da-5899-933d-7e05d901e3eb",
"leftValue": "={{ $json.found }}",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"rightValue": ""
}
],
"combinator": "and"
},
"options": {}
},
"id": "21f335b6-8a0a-5331-b8df-43d3c9f9aafc",
"name": "FAQ found?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1160,
-40
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $('Settings').first().json.llm_url }}/chat/completions",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: $('Settings').first().json.model, temperature: 0, messages: [ { role: 'system', content: \"Write a polite 2-sentence Korean reply using only the FAQ answer. End with the FAQ id in parentheses.\" }, { role: 'user', content: '문의: ' + $('Normalize and search FAQ').first().json.text + '\\nFAQ: ' + JSON.stringify($('Normalize and search FAQ').first().json.faq) } ] }) }}",
"options": {
"timeout": 180000
},
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi"
},
"id": "a1e86429-1ee7-585c-bdaa-f1aa0d10f49b",
"name": "LLM draft reply",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1400,
-140
],
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
}
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "1e3762b6-7dae-5e2f-9538-8a5b0f5dcae0",
"name": "category",
"value": "={{ $('Normalize and search FAQ').first().json.category }}",
"type": "string"
},
{
"id": "230f5390-e5c9-5e6a-90ec-03e6965652f7",
"name": "faq_id",
"value": "={{ $('Normalize and search FAQ').first().json.faq.id }}",
"type": "string"
},
{
"id": "1f78eb57-6478-5590-94f9-bacb9b2c89b9",
"name": "route",
"value": "자동 답변",
"type": "string"
},
{
"id": "756cec4c-d91f-516a-9d9f-ae74098227e9",
"name": "reply",
"value": "={{ $json.choices[0].message.content.trim() }}",
"type": "string"
}
]
},
"includeOtherFields": false,
"options": {}
},
"id": "da512682-1045-5a8e-83d1-52975eb08e97",
"name": "Auto reply",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1640,
-140
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "bf4ced71-6e32-5ed2-ad7d-4124d462e3d2",
"name": "category",
"value": "={{ $json.category }}",
"type": "string"
},
{
"id": "79bffc37-152f-5b3e-9018-e30121faed11",
"name": "faq_id",
"value": "",
"type": "string"
},
{
"id": "86fde04e-b2a1-5782-97c1-caba13a9ed10",
"name": "route",
"value": "담당자 확인",
"type": "string"
},
{
"id": "d760805f-73f1-5d0a-a7cf-8eb67d03e978",
"name": "reply",
"value": "관련 안내를 찾지 못해 담당자가 확인 후 답변드리겠습니다.",
"type": "string"
}
]
},
"includeOtherFields": false,
"options": {}
},
"id": "604eb70d-bff2-5270-b1a4-f94a3a6c681d",
"name": "Hand off to staff",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1400,
80
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"id": "2de33965-f060-5f8b-bd91-2a7c72a1e618",
"name": "Send result",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1880,
-40
]
}
],
"connections": {
"AI request webhook": {
"main": [
[
{
"node": "Settings",
"type": "main",
"index": 0
}
]
]
},
"Settings": {
"main": [
[
{
"node": "Has text?",
"type": "main",
"index": 0
}
]
]
},
"Has text?": {
"main": [
[
{
"node": "LLM classify",
"type": "main",
"index": 0
}
],
[
{
"node": "Reject empty",
"type": "main",
"index": 0
}
]
]
},
"LLM classify": {
"main": [
[
{
"node": "Normalize and search FAQ",
"type": "main",
"index": 0
}
]
]
},
"Normalize and search FAQ": {
"main": [
[
{
"node": "FAQ found?",
"type": "main",
"index": 0
}
]
]
},
"FAQ found?": {
"main": [
[
{
"node": "LLM draft reply",
"type": "main",
"index": 0
}
],
[
{
"node": "Hand off to staff",
"type": "main",
"index": 0
}
]
]
},
"LLM draft reply": {
"main": [
[
{
"node": "Auto reply",
"type": "main",
"index": 0
}
]
]
},
"Auto reply": {
"main": [
[
{
"node": "Send result",
"type": "main",
"index": 0
}
]
]
},
"Hand off to staff": {
"main": [
[
{
"node": "Send result",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"pinData": {},
"id": "493c961026875d"
}