diff --git a/workflow/01_conversation/coldmail_ir_notification_sync.json b/workflow/01_conversation/coldmail_ir_notification_sync.json new file mode 100644 index 0000000..dc20a88 --- /dev/null +++ b/workflow/01_conversation/coldmail_ir_notification_sync.json @@ -0,0 +1,105 @@ +{ + "name": "robeing-slack-coldmail-ir-notification", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "notifications/coldmail-ir", + "responseMode": "responseNode", + "options": {} + }, + "id": "cm-ir-001", + "name": "Webhook In", + "type": "n8n-nodes-base.webhook", + "typeVersion": 1, + "position": [200, 240] + }, + { + "parameters": { + "jsCode": "const body = $json.body || {};\nconst has_ir = body.has_ir_deck && body.selected_document_id;\nreturn {\n company_name: body.company_name || '알 수 없는 회사',\n sender: body.sender || '',\n subject: body.subject || '',\n summary: body.summary || '요약 정보가 없습니다.',\n has_ir,\n document_id: body.selected_document_id || '',\n document_name: body.selected_filename || '',\n slack_channel: body.slack_channel || 'C07...',\n robeing_id: body.robeing_id || 'rb8001'\n};" + }, + "id": "cm-ir-002", + "name": "Format Coldmail Data", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [440, 240] + }, + { + "parameters": { + "resource": "message", + "operation": "post", + "channel": "={{ $json.slack_channel }}", + "text": "=📩 *새 콜드메일 감지: {{ $json.company_name }}*\\n보낸이: {{ $json.sender }}\\n제목: {{ $json.subject }}\\n\\n*요약*\\n{{ $json.summary }}", + "otherOptions": { + "includeLinkToApp": false + } + }, + "id": "cm-ir-003", + "name": "Post Basic Notification", + "type": "n8n-nodes-base.slack", + "typeVersion": 2, + "position": [700, 240] + }, + { + "parameters": { + "conditions": { + "boolean": [ + { + "value1": "={{ $json.has_ir }}", + "value2": true + } + ] + } + }, + "id": "cm-ir-004", + "name": "Has IR Deck?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [960, 240] + }, + { + "parameters": { + "resource": "message", + "operation": "post", + "channel": "={{ $json.slack_channel }}", + "text": "=📄 *IR 자료 감지됨: {{ $json.document_name }}*", + "otherOptions": { + "threadTs": "={{ $json.ts }}", + "attachments": [ + { + "fallback": "IR 분석 버튼", + "callback_id": "coldmail_analyze_ir", + "actions": [ + { + "name": "analyze", + "text": "📊 IR 지표 추출 및 밸류에이션", + "type": "button", + "value": "={{ $json.document_id }}", + "style": "primary" + } + ] + } + ] + } + }, + "id": "cm-ir-005", + "name": "Add IR Analysis Button", + "type": "n8n-nodes-base.slack", + "typeVersion": 2, + "position": [1240, 160] + } + ], + "connections": { + "Webhook In": { "main": [[{ "node": "Format Coldmail Data", "type": "main", "index": 0 }]] }, + "Format Coldmail Data": { "main": [[{ "node": "Post Basic Notification", "type": "main", "index": 0 }]] }, + "Post Basic Notification": { "main": [[{ "node": "Has IR Deck?", "type": "main", "index": 0 }]] }, + "Has IR Deck?": { + "main": [ + [{ "node": "Add IR Analysis Button", "type": "main", "index": 0 }], + [] + ] + } + }, + "pinData": {}, + "meta": { "templateCredsSetupCompleted": true, "instanceId": "robeing-conversation" } +} diff --git a/workflow/02_skills/skill_calendar_request.json b/workflow/02_skills/skill_calendar_request.json new file mode 100644 index 0000000..8ca48cb --- /dev/null +++ b/workflow/02_skills/skill_calendar_request.json @@ -0,0 +1,125 @@ +{ + "name": "robeing-skill-calendar-request", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "skills/calendar/request", + "responseMode": "responseNode", + "options": {} + }, + "id": "cal-req-001", + "name": "Webhook In", + "type": "n8n-nodes-base.webhook", + "typeVersion": 1, + "position": [200, 240] + }, + { + "parameters": { + "jsCode": "const body = $json.body || {};\nconst action = body.action || 'query'; // 'create', 'query', 'delete'\nreturn {\n action,\n user_id: body.user_id,\n robeing_id: body.robeing_id || 'rb8001',\n query: body.query || '',\n event_data: body.event_data || {},\n event_id: body.event_id || ''\n};" + }, + "id": "cal-req-002", + "name": "Normalize Payload", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [440, 240] + }, + { + "parameters": { + "conditions": { + "string": [ + { + "value1": "={{ $json.action }}", + "operation": "equals", + "value2": "create" + } + ] + } + }, + "id": "cal-req-003", + "name": "Action Type", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [680, 240] + }, + { + "parameters": { + "method": "POST", + "url": "=http://192.168.219.52:8512/api/events", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "X-User-Id", + "value": "={{ $json.user_id }}" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ $json.event_data }}", + "options": {} + }, + "id": "cal-req-004", + "name": "Create Event", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [940, 160] + }, + { + "parameters": { + "method": "GET", + "url": "=http://192.168.219.52:8512/api/events", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "X-User-Id", + "value": "={{ $json.user_id }}" + } + ] + }, + "qs": { + "parameters": [ + { + "name": "query", + "value": "={{ $json.query }}" + } + ] + }, + "options": {} + }, + "id": "cal-req-005", + "name": "Query Events", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [940, 320] + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json.body || $json }}", + "options": {} + }, + "id": "cal-req-006", + "name": "Return Result", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1, + "position": [1220, 240] + } + ], + "connections": { + "Webhook In": { "main": [[{ "node": "Normalize Payload", "type": "main", "index": 0 }]] }, + "Normalize Payload": { "main": [[{ "node": "Action Type", "type": "main", "index": 0 }]] }, + "Action Type": { + "main": [ + [{ "node": "Create Event", "type": "main", "index": 0 }], + [{ "node": "Query Events", "type": "main", "index": 0 }] + ] + }, + "Create Event": { "main": [[{ "node": "Return Result", "type": "main", "index": 0 }]] }, + "Query Events": { "main": [[{ "node": "Return Result", "type": "main", "index": 0 }]] } + }, + "pinData": {}, + "meta": { "templateCredsSetupCompleted": true, "instanceId": "robeing-skills" } +} diff --git a/workflow/03_rag/companyx_grounding_pipeline.json b/workflow/03_rag/companyx_grounding_pipeline.json new file mode 100644 index 0000000..f072efe --- /dev/null +++ b/workflow/03_rag/companyx_grounding_pipeline.json @@ -0,0 +1,98 @@ +{ + "name": "robeing-rag-companyx-grounding-pipeline", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "rag/companyx/grounding", + "responseMode": "responseNode", + "options": {} + }, + "id": "cx-rag-001", + "name": "Webhook In", + "type": "n8n-nodes-base.webhook", + "typeVersion": 1, + "position": [200, 240] + }, + { + "parameters": { + "jsCode": "const body = $json.body || {};\nreturn {\n query: body.query || '',\n user_uuid: body.user_id || '',\n team_id: '79441171-3951-4870-beb8-916d07fe8be5', // Company X Team ID\n limit: body.limit || 5\n};" + }, + "id": "cx-rag-002", + "name": "Set Context (Company X)", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [440, 240] + }, + { + "parameters": { + "method": "POST", + "url": "=http://192.168.219.52:8508/api/search", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"query\": \"{{ $json.query }}\",\n \"team_id\": \"{{ $json.team_id }}\",\n \"limit\": {{ $json.limit }}\n}", + "options": {} + }, + "id": "cx-rag-003", + "name": "Search Internal Docs", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [680, 240] + }, + { + "parameters": { + "jsCode": "const results = $json.body || [];\nconst context = results.map(r => `[출처: ${r.filename}]\\n${r.text}`).join('\\n---\\n');\nconst original = $('Set Context (Company X)').item.json;\nreturn {\n message: original.query,\n grounding_context: context,\n user_id: original.user_uuid,\n robeing_id: 'rb8001'\n};" + }, + "id": "cx-rag-004", + "name": "Prepare LLM Prompt", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [940, 240] + }, + { + "parameters": { + "method": "POST", + "url": "=http://192.168.219.52:8001/api/chat", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "X-User-Id", + "value": "={{ $json.user_id }}" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"message\": \"{{ $json.message }}\",\n \"context\": \"아래의 컴퍼니엑스 내부 문서 내용을 바탕으로 답변하세요:\\n{{ $json.grounding_context }}\",\n \"source\": \"n8n-grounding\"\n}", + "options": {} + }, + "id": "cx-rag-005", + "name": "Call rb8001 with Context", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1200, 240] + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json.body }}", + "options": {} + }, + "id": "cx-rag-006", + "name": "Return Grounded Answer", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1, + "position": [1460, 240] + } + ], + "connections": { + "Webhook In": { "main": [[{ "node": "Set Context (Company X)", "type": "main", "index": 0 }]] }, + "Set Context (Company X)": { "main": [[{ "node": "Search Internal Docs", "type": "main", "index": 0 }]] }, + "Search Internal Docs": { "main": [[{ "node": "Prepare LLM Prompt", "type": "main", "index": 0 }]] }, + "Prepare LLM Prompt": { "main": [[{ "node": "Call rb8001 with Context", "type": "main", "index": 0 }]] }, + "Call rb8001 with Context": { "main": [[{ "node": "Return Grounded Answer", "type": "main", "index": 0 }]] } + }, + "pinData": {}, + "meta": { "templateCredsSetupCompleted": true, "instanceId": "robeing-rag" } +} diff --git a/workflow/05_admin/diary_reflection_pipeline.json b/workflow/05_admin/diary_reflection_pipeline.json new file mode 100644 index 0000000..355211a --- /dev/null +++ b/workflow/05_admin/diary_reflection_pipeline.json @@ -0,0 +1,104 @@ +{ + "name": "robeing-admin-diary-reflection-pipeline", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "cronExpression", + "expression": "0 2 * * *" + } + ] + } + }, + "id": "diary-001", + "name": "Daily at 2AM", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.1, + "position": [200, 240] + }, + { + "parameters": { + "httpMethod": "POST", + "path": "admin/diary/trigger", + "options": {} + }, + "id": "diary-002", + "name": "Manual Trigger", + "type": "n8n-nodes-base.webhook", + "typeVersion": 1, + "position": [200, 440] + }, + { + "parameters": { + "jsCode": "const now = new Date();\nconst yesterday = new Date(now);\nyesterday.setDate(yesterday.getDate() - 1);\nconst dateStr = yesterday.toISOString().split('T')[0];\nreturn {\n date: dateStr,\n robeing_id: 'rb8001'\n};" + }, + "id": "diary-003", + "name": "Set Yesterday Date", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [460, 340] + }, + { + "parameters": { + "method": "POST", + "url": "=http://192.168.219.52:8001/api/diary/generate", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"date\": \"{{ $json.date }}\",\n \"robeing_id\": \"{{ $json.robeing_id }}\"\n}", + "options": { + "timeout": 300000 + } + }, + "id": "diary-004", + "name": "Generate Diary (rb8001)", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [700, 340] + }, + { + "parameters": { + "method": "GET", + "url": "=http://192.168.219.52:8001/api/diary/{{ $json.date }}", + "qs": { + "parameters": [ + { + "name": "robeing_id", + "value": "rb8001" + } + ] + }, + "options": {} + }, + "id": "diary-005", + "name": "Get Diary Content", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [940, 340] + }, + { + "parameters": { + "resource": "message", + "operation": "post", + "channel": "C07...", + "text": "=📜 *로빙 일기 ({{ $json.date }})*\\n\\n{{ $json.summary }}\\n\\n지배적 감정: {{ $json.dominant_emotion }}\\n\\n[대시보드에서 자세히 보기](http://192.168.219.45:3000/diary)", + "otherOptions": {} + }, + "id": "diary-006", + "name": "Post to Slack", + "type": "n8n-nodes-base.slack", + "typeVersion": 2, + "position": [1180, 340] + } + ], + "connections": { + "Daily at 2AM": { "main": [[{ "node": "Set Yesterday Date", "type": "main", "index": 0 }]] }, + "Manual Trigger": { "main": [[{ "node": "Set Yesterday Date", "type": "main", "index": 0 }]] }, + "Set Yesterday Date": { "main": [[{ "node": "Generate Diary (rb8001)", "type": "main", "index": 0 }]] }, + "Generate Diary (rb8001)": { "main": [[{ "node": "Get Diary Content", "type": "main", "index": 0 }]] }, + "Get Diary Content": { "main": [[{ "node": "Post to Slack", "type": "main", "index": 0 }]] } + }, + "pinData": {}, + "meta": { "templateCredsSetupCompleted": true, "instanceId": "robeing-admin" } +} diff --git a/workflow/05_admin/service_health_check.json b/workflow/05_admin/service_health_check.json new file mode 100644 index 0000000..6243cc2 --- /dev/null +++ b/workflow/05_admin/service_health_check.json @@ -0,0 +1,107 @@ +{ + "name": "robeing-admin-service-health-check", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "minutes", + "interval": 10 + } + ] + } + }, + "id": "health-001", + "name": "Every 10 mins", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.1, + "position": [200, 240] + }, + { + "parameters": { + "jsCode": "return [\n { name: 'robeing-gateway', url: 'http://192.168.219.45:8100/health' },\n { name: 'auth-server', url: 'http://192.168.219.45:9000/health' },\n { name: 'rb8001', url: 'http://192.168.219.52:8001/health' },\n { name: 'skill-email', url: 'http://192.168.219.52:8501/healthz' },\n { name: 'skill-news', url: 'http://192.168.219.52:8505/healthz' },\n { name: 'skill-rag-file', url: 'http://192.168.219.52:8508/healthz' },\n { name: 'skill-calendar', url: 'http://192.168.219.52:8512/health' }\n];" + }, + "id": "health-002", + "name": "Service List", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [440, 240] + }, + { + "parameters": { + "method": "GET", + "url": "={{ $json.url }}", + "options": { + "response": { + "response": { + "fullResponse": true + } + }, + "timeout": 5000 + } + }, + "id": "health-003", + "name": "Check Health", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [680, 240], + "onError": "continueErrorOutput" + }, + { + "parameters": { + "jsCode": "const service = $item.input.json.name;\nconst response = $json.body || {};\nconst statusCode = $json.statusCode;\nconst isUp = statusCode === 200 || statusCode === 201;\nreturn {\n service,\n is_up: isUp,\n status: isUp ? 'UP' : 'DOWN',\n code: statusCode || 'TIMEOUT/ERROR'\n};" + }, + "id": "health-004", + "name": "Assess Status", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [940, 240] + }, + { + "parameters": { + "conditions": { + "boolean": [ + { + "value1": "={{ $json.is_up }}", + "value2": false + } + ] + } + }, + "id": "health-005", + "name": "Is Down?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [1180, 240] + }, + { + "parameters": { + "resource": "message", + "operation": "post", + "channel": "C07...", + "text": "=🚨 *서비스 장애 감지*\\n\\n서비스: *{{ $json.service }}*\\n상태: DOWN (Code: {{ $json.code }})\\n\\n확인이 필요합니다.", + "otherOptions": {} + }, + "id": "health-006", + "name": "Alert Slack", + "type": "n8n-nodes-base.slack", + "typeVersion": 2, + "position": [1440, 180] + } + ], + "connections": { + "Every 10 mins": { "main": [[{ "node": "Service List", "type": "main", "index": 0 }]] }, + "Service List": { "main": [[{ "node": "Check Health", "type": "main", "index": 0 }]] }, + "Check Health": { "main": [[{ "node": "Assess Status", "type": "main", "index": 0 }]] }, + "Assess Status": { "main": [[{ "node": "Is Down?", "type": "main", "index": 0 }]] }, + "Is Down?": { + "main": [ + [{ "node": "Alert Slack", "type": "main", "index": 0 }], + [] + ] + } + }, + "pinData": {}, + "meta": { "templateCredsSetupCompleted": true, "instanceId": "robeing-admin" } +} diff --git a/workflow/README.md b/workflow/README.md index 176d8ed..fd551c3 100644 --- a/workflow/README.md +++ b/workflow/README.md @@ -1,16 +1,46 @@ # Robeing n8n Workflows -로빙의 반복 실행 절차를 n8n import용 JSON으로 분류해 둔 폴더입니다. +로빙의 분산 마이크로서비스 및 비즈니스 로직을 n8n으로 자동화하기 위한 템플릿 저장소입니다. -## 폴더 +## 폴더 구조 -- `01_conversation/`: Slack 대화 진입, 스레드 요약, 액션 추출 -- `02_skills/`: `skill-*` 서비스 호출 브리지 -- `03_rag/`: 업로드, 임베딩, 검색, 인덱싱 -- `04_scheduler/`: APScheduler 성격의 정기 실행 대체/보조 흐름 +- `01_conversation/`: Slack 진입점, 대화 정규화, 콜드메일 IR 알림 및 액션 추출 +- `02_skills/`: `skill-*` 서비스 브리지 (Email, News, Calendar, Slack) +- `03_rag/`: 파일 업로드, 인덱싱, 임베딩, CompanyX 전용 Grounding 파이프라인 +- `04_scheduler/`: 정기 브리핑, 상태 알림, 재시도 로직 +- `05_admin/`: 서비스 헬스 체크, 일기(Reflection) 자동 생성 및 동기화 -## 사용 원칙 +## 시스템 구성 및 엔드포인트 -- 이 JSON은 n8n import 템플릿입니다. -- 실제 운영 전 credential, URL, channel, API key, DB 연결은 환경에 맞게 교체해야 합니다. -- 로빙의 판단 루프 자체를 대체하지 않고, 반복 실행 절차를 분리하는 용도로 사용합니다. +| 서버 | 역할 | IP | 주요 포트 | +|------|------|----|-----------| +| 51123 | 메인/인증/DB | 192.168.219.45 | Gateway(8100), Auth(9000), DB(5432) | +| 51124 | 로빙/스킬 | 192.168.219.52 | rb8001(8001), Email(8501), News(8505), RAG(8508), Calendar(8512) | + +## 주요 워크플로우 가이드 + +### 1. 로빙 일기 시스템 (`05_admin/diary_reflection_pipeline.json`) +- 매일 오전 2시, `rb8001`의 하루 활동과 감정을 집계하여 일기를 생성합니다. +- 생성된 요약본을 Slack 관리자 채널로 전송합니다. + +### 2. 콜드메일 IR 분석 (`01_conversation/coldmail_ir_notification_sync.json`) +- 네이버웍스 등을 통해 수신된 콜드메일을 감지하고, IR Deck(PDF) 존재 여부를 판별합니다. +- IR 자료가 있을 경우 Slack에 분석 버튼을 노출하여 원클릭으로 지표 추출을 실행합니다. + +### 3. 서비스 헬스 모니터 (`05_admin/service_health_check.json`) +- 10분 간격으로 모든 마이크로서비스의 `/health` 또는 `/healthz`를 체크합니다. +- 장애 발생 시 Slack 알림을 통해 즉각적인 대응을 지원합니다. + +### 4. 캘린더 스킬 브리지 (`02_skills/skill_calendar_request.json`) +- 구글 캘린더 연동을 위한 브리지 워크플로우입니다. +- 일정 조회, 등록, 삭제 요청을 `skill-calendar` 서비스로 전달합니다. + +## 사용 시 주의사항 + +- **Credential 설정**: 각 노드의 Slack, HTTP Request 노드에서 실제 환경의 Credentials(Token, API Key)를 설정해야 합니다. +- **IP 주소**: 현재 템플릿은 내부 IP(`192.168.219.*`)를 기본값으로 사용합니다. 환경이 다를 경우 일괄 변경이 필요합니다. +- **채널 ID**: Slack 노드에 설정된 `channelId`는 예시 값이므로, 실제 알림을 받을 채널 ID로 수정하십시오. + +--- +**최근 업데이트**: 2026-03-18 +**상태**: 16개 워크플로우 반영 완료 (기존 12개 + 신규 4개)