From 9cb0145a4ec06b0c43b42c9a0bb9607e6f55a4b2 Mon Sep 17 00:00:00 2001 From: happybell80 Date: Mon, 6 Apr 2026 07:57:58 +0900 Subject: [PATCH] =?UTF-8?q?docs:=2001=5Fconversation=20+=2002=5Fskills=20?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=ED=98=84=ED=96=89=ED=99=94=20=E2=80=94=20n8n=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0,=20SKILL.md+executor=20=EA=B8=B0=EC=A4=80=20?= =?UTF-8?q?=EC=9E=AC=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - coldmail_ir_notification_sync: LangGraph+APScheduler 기반으로 전면 재작성 - slack_action_extractor_request: v2 IntentClassifier→executor 흐름 기준 재작성 - slack_thread_summary_request: 동일 - message_flow_v2: _format_as_robeing() 해석 단계 보완 - skill_calendar/email/news_request: SKILL.md→executor 직접 호출 기준 재작성 - skill_slack_send_message_bridge: slack_sdk 직접 호출 + skill-slack HTTP 이중 경로 기술 - slack_direct_api_send: grounding 개선 사항 반영 Refs: DOCS#8 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../coldmail_ir_notification_sync.md | 96 ++++++++++++++++--- workflow/01_conversation/message_flow_v2.md | 17 +++- .../slack_action_extractor_request.md | 54 ++++++++--- .../slack_thread_summary_request.md | 54 ++++++++--- workflow/02_skills/skill_calendar_request.md | 70 ++++++++++---- .../02_skills/skill_email_send_request.md | 73 +++++++++++--- .../02_skills/skill_news_briefing_request.md | 63 +++++++++--- .../skill_slack_send_message_bridge.md | 70 +++++++++++--- workflow/02_skills/slack_direct_api_send.md | 59 ++++++------ 9 files changed, 419 insertions(+), 137 deletions(-) diff --git a/workflow/01_conversation/coldmail_ir_notification_sync.md b/workflow/01_conversation/coldmail_ir_notification_sync.md index cf196f8..3753d43 100644 --- a/workflow/01_conversation/coldmail_ir_notification_sync.md +++ b/workflow/01_conversation/coldmail_ir_notification_sync.md @@ -1,26 +1,94 @@ +--- +type: workflow +tags: [workflow, coldmail, langgraph, scheduler, ir-deck] +last_updated: 2026-04-06 +--- + # coldmail_ir_notification_sync 워크플로우 ## 목적 -콜드메일 수신 시 IR 자료 포함 여부를 판별하고, Slack에 요약 알림 + IR 분석 버튼을 게시한다. + +콜드메일을 자동 감지하여 IR 자료 포함 여부를 판별하고, Slack에 요약 알림을 게시한다. 신뢰도에 따라 자동 등록 또는 사용자 확인을 거친다. + +## 현행 아키텍처 + +- **스케줄러**: APScheduler DB 기반 cron (`coldmail_briefing` 잡) +- **워크플로우 엔진**: LangGraph StateGraph + AsyncSqliteSaver 체크포인트 +- **Slack 전송**: `skill-slack` 서비스 HTTP 호출 (`SKILL_SLACK_URL` 환경변수) + +n8n은 사용하지 않는다. ## 흐름 + ``` -Webhook In → Format Coldmail Data → Post Basic Notification → Has IR Deck? → (true) Add IR Analysis Button - → (false) 종료 +APScheduler cron (평일 09:05) + → coldmail_briefing.py._run_coldmail_briefing() + → briefing_window 계산 (월요일 72h, 화~금 24h) + → LangGraph 워크플로우 실행: + fetch → filter → process → wait_confirmation (interrupt) → confirm → send → END ``` -## 주요 노드 -| 노드 | 설명 | -|---|---| -| Webhook In | `POST /notifications/coldmail-ir` 수신 | -| Format Coldmail Data | 회사명, 발신자, 제목, 요약, IR 문서 유무 추출 | -| Post Basic Notification | Slack 채널에 콜드메일 요약 게시 | -| Has IR Deck? | `has_ir_deck && selected_document_id` 조건 분기 | -| Add IR Analysis Button | 스레드에 IR 지표 추출/밸류에이션 버튼 추가 | +## 단계별 상세 + +### 1. 스케줄러 트리거 + +| 항목 | 내용 | +|------|------| +| 코드 | `app/scheduler/jobs/coldmail_briefing.py` | +| 설정 | `settings.COLDMAIL_BRIEFING_ENABLED`, `settings.COLDMAIL_BRIEFING_SCHEDULE` | +| 스케줄 | cron 표현식 → APScheduler 형식으로 변환 | +| 사용자 | `settings.NAVERWORKS_COMPANY_EMAIL` → UUID 조회 | + +### 2. LangGraph 워크플로우 + +| 항목 | 내용 | +|------|------| +| 코드 | `app/services/workflows/coldmail_workflow.py` | +| 상태 | `ColdmailState` TypedDict (emails, coldmail_candidates, processed_results, waiting_confirmation 등) | +| 체크포인트 | `AsyncSqliteSaver` (`settings.LANGGRAPH_STATE_DIR` / `settings.LANGGRAPH_SQLITE_FILE`) | +| thread_id | `coldmail:{user_id}:{date}` | + +### 3. 노드 ��명 + +| 노드 | 함수 | 역할 | +|------|------|------| +| fetch | `fetch_node` | `coldmail_email_fetcher.fetch_emails()` 호출. 사전 주입 이메일이 있으면 그대로 사용 | +| filter | `filter_node` | `coldmail_hybrid_filter.hybrid_coldmail_filter()` — 하이브리드 필터(첨부파일+본문) 적용 | +| process | `process_node` | 신뢰도 95% 이상 자동 처리, 미만은 `waiting_confirmation`에 추가 | +| wait_confirmation | `wait_confirmation_node` | `interrupt()` 호출하여 워크플로우 일시정지. 사용자 확인/거부 대기 | +| confirm | `confirm_node` | 확인된 이메일을 리스트에 등록하고 로딩 메시지 업데이트 | +| send | `send_node` | Slack 채널에 처리 결과 요약 전송 | + +### 4. 조건부 라우팅 + +| 분기점 | 조건 | 다음 노드 | +|--------|------|-----------| +| filter 후 | coldmail_candidates 있음 | process | +| filter 후 | coldmail_candidates 없음 | END | +| wait_confirmation 후 | confirmed_email_id 있음 | confirm | +| wait_confirmation 후 | waiting_confirmation 남음 | wait_confirmation (루프) | +| wait_confirmation 후 | 둘 다 없음 | send | +| confirm 후 | waiting_confirmation 남음 | wait_confirmation | +| confirm 후 | 없음 | send | ## 엔드포인트 -- 인바운드: `POST /notifications/coldmail-ir` (n8n webhook) -- 아웃바운드: Slack `chat.postMessage` (기본 알림 + IR 버튼) + +- **인바운드**: 없음 (스케줄러가 내부 직접 호출) +- **아웃바운드**: `POST {SKILL_SLACK_URL}/api/v1/send` — Slack 요약 전송 +- **아웃바운드**: `POST {SKILL_SLACK_URL}/api/v1/update` — 로딩 메시지 업데이트 + +## 환경변수 + +| 변수 | 용도 | +|------|------| +| `COLDMAIL_CHANNEL_ID` | Slack 알림 채널 | +| `SKILL_SLACK_URL` | skill-slack 서비스 URL | +| `SKILL_SLACK_API_KEY` / `SERVICE_API_KEY` | skill-slack 인증 | +| `NAVERWORKS_COMPANY_EMAIL` | 이메일 조회 대상 사용자 | +| `LANGGRAPH_STATE_DIR` | 체크포인트 DB 디렉터리 | +| `LANGGRAPH_SQLITE_FILE` | 체크포인트 DB 파일명 | ## 관련 문서 -- [slack_basic_dialogue](./slack_basic_dialogue.md) + +- [message_flow_v2.md](./message_flow_v2.md) — 대화 처리 흐름 (v2) +- [skill-slack SKILL.md](/home/admin/robeing/DOCS/skills/skill-slack/SKILL.md) — Slack 스킬 계약 diff --git a/workflow/01_conversation/message_flow_v2.md b/workflow/01_conversation/message_flow_v2.md index 1a96c68..9de44b1 100644 --- a/workflow/01_conversation/message_flow_v2.md +++ b/workflow/01_conversation/message_flow_v2.md @@ -1,7 +1,7 @@ --- type: workflow tags: [workflow, message-flow, rb8001, intent, skill, executor] -last_updated: 2026-04-03 +last_updated: 2026-04-06 upper_principle: - /home/admin/0_VALUE/20_Governance/coding-principles.md - /home/admin/0_VALUE/20_Governance/work-system-principle.md @@ -64,11 +64,22 @@ upper_principle: | internal_python | 내부 handler 함수 직접 호출 | | 미등록 스킬 | `{"success": false, "error": "미등록"}` 반환 | -### 5. 응답 +### 5. 해석 (_format_as_robeing) | 항목 | 내용 | |------|------| -| 결과 조립 | executor 결과를 사용자 메시지로 변환 | +| 코드 | `app/services/message_service_v2.py:_format_as_robeing()` | +| 입력 | 사용자 원문, IntentResult, executor 실행 결과(JSON) | +| 방식 | DB 프롬프트(`response_formatter_system`) + LLM 호출 (model: `settings.DEFAULT_LLM_MODEL`, temp: 0.3, max_tokens: 200) | +| 출력 | 자연어 문자열. 스킬 결과를 로빙의 말투로 변환 | +| 실패 시 | 원본 JSON 문자열 그대로 반환 | +| 근거 | coding-principles "응답 형식은 접속 대상이 결정한다" — 사람이 대상이므로 문자열 | + +### 6. 응답 + +| 항목 | 내용 | +|------|------| +| 대화 저장 | `_save_conversation()` — DB에 user/assistant 메시지 저장 | | 채널 전달 | Slack reply / 웹 응답 | ## 스킬 참여 예시 diff --git a/workflow/01_conversation/slack_action_extractor_request.md b/workflow/01_conversation/slack_action_extractor_request.md index 330a481..a8960d9 100644 --- a/workflow/01_conversation/slack_action_extractor_request.md +++ b/workflow/01_conversation/slack_action_extractor_request.md @@ -1,27 +1,51 @@ +--- +type: workflow +tags: [workflow, slack, action-extractor, skill-slack, intent] +last_updated: 2026-04-06 +--- + # slack_action_extractor_request 워크플로우 ## 목적 -Slack 메시지에서 "할 일/액션/todo/action" 의도를 감지하면, skill-slack의 액션 추출 API를 호출하여 결과를 스레드에 회신한다. + +Slack 메시지에서 "할 일/액션/todo" 의도를 감지하면, skill-slack의 액션 추출 API를 호출하여 결과를 반환한다. + +## 현행 아키텍처 + +v2 메시지 흐름(IntentClassifier → executor)을 통해 처리된다. n8n Trigger, 별도 webhook은 사용하지 않는다. ## 흐름 + ``` -Slack Trigger → Detect Action Intent → Action Request? → (true) Call skill-slack Extract Actions → Build Action Reply → Reply to Slack - → (false) 종료 +사용자 발화 ("할 일 뽑아줘", "액션 아이템 정리해줘") + → message_service_v2.route_message() + → IntentClassifier.classify() + — SKILL.md 본문에서 skill-slack 트리거 매칭 + — IntentResult(skill="skill-slack", action="action_extractor", slots=...) + → executor.execute() + — registry에서 SKILL_SLACK_URL 환경변수 해소 + — POST {SKILL_SLACK_URL}/api/v1/actions (httpx) + → _format_as_robeing() — 결과를 자연어로 변환 + → 채널 응답 ``` -## 주요 노드 -| 노드 | 설명 | -|---|---| -| Slack Trigger | 채널 메시지 이벤트 수신 | -| Detect Action Intent | 정규식으로 액션 키워드 감지, channel/user 유효성 확인 | -| Call skill-slack Extract Actions | `POST :8502/api/v1/extract-actions` 호출 (24h, auto_assign, priority_detection) | -| Build Action Reply | 추출된 액션 아이템을 번호 목록 텍스트로 포맷 | -| Reply to Slack | 원래 스레드에 결과 회신 | +## 주요 코드 경로 + +| 단계 | 코드 | +|------|------| +| 진입 | `app/services/message_service_v2.py:route_message()` | +| 의도분류 | `app/services/brain/intent_classifier.py:IntentClassifier.classify()` | +| 스킬 실행 | `app/services/skills/executor.py:execute()` | +| URL 해소 | `app/services/skills/registry.py:SkillMeta.resolve_url()` | +| 스킬 정의 | `DOCS/skills/skill-slack/SKILL.md` (트리거: `action_extractor`) | ## 엔드포인트 -- 아웃바운드: `POST http://192.168.219.52:8502/api/v1/extract-actions` -- 아웃바운드: Slack `chat.postMessage` (스레드 회신) + +- **아웃바운드**: `POST {SKILL_SLACK_URL}/api/v1/actions` — skill-slack 서비스 +- `SKILL_SLACK_URL` 환경변수로 해소. IP 하드코딩 없음 ## 관련 문서 -- [slack_thread_summary_request](./slack_thread_summary_request.md) -- [slack_basic_dialogue](./slack_basic_dialogue.md) + +- [message_flow_v2.md](./message_flow_v2.md) — v2 메시지 흐름 +- [slack_thread_summary_request.md](./slack_thread_summary_request.md) — 스레드 요약 (같은 skill-slack) +- [skill-slack SKILL.md](/home/admin/robeing/DOCS/skills/skill-slack/SKILL.md) — 스킬 계약 diff --git a/workflow/01_conversation/slack_thread_summary_request.md b/workflow/01_conversation/slack_thread_summary_request.md index 002f150..75be49b 100644 --- a/workflow/01_conversation/slack_thread_summary_request.md +++ b/workflow/01_conversation/slack_thread_summary_request.md @@ -1,27 +1,51 @@ +--- +type: workflow +tags: [workflow, slack, thread-summary, skill-slack, intent] +last_updated: 2026-04-06 +--- + # slack_thread_summary_request 워크플로우 ## 목적 -Slack 메시지에서 "요약/정리/digest/summary" 의도를 감지하면, skill-slack의 요약 API를 호출하여 결과를 스레드에 회신한다. + +Slack 메시지에서 "요약/정리/digest/summary" 의도를 감지하면, skill-slack의 요약 API를 호출하여 결과를 반환한다. + +## 현행 아키텍처 + +v2 메시지 흐름(IntentClassifier → executor)을 통해 처리된다. n8n Trigger, 별도 webhook은 사용하지 않는다. ## 흐름 + ``` -Slack Trigger → Detect Summary Intent → Summary Request? → (true) Call skill-slack Summarize → Build Summary Reply → Reply to Slack - → (false) 종료 +사용자 발화 ("스레드 요약해줘", "대화 정리해줘") + → message_service_v2.route_message() + → IntentClassifier.classify() + — SKILL.md 본문에서 skill-slack 트리거 매칭 + — IntentResult(skill="skill-slack", action="slack_thread", slots=...) + → executor.execute() + — registry에서 SKILL_SLACK_URL 환경변수 해소 + — POST {SKILL_SLACK_URL}/api/v1/summarize (httpx) + → _format_as_robeing() — 결과를 자연어로 변환 + → 채널 응답 ``` -## 주요 노드 -| 노드 | 설명 | -|---|---| -| Slack Trigger | 채널 메시지 이벤트 수신 | -| Detect Summary Intent | 정규식으로 요약 키워드 감지, channel/user 유효성 확인 | -| Call skill-slack Summarize | `POST :8502/api/v1/summarize` 호출 (24h, include_files=true) | -| Build Summary Reply | 요약 텍스트 추출 | -| Reply to Slack | 원래 스레드에 결과 회신 | +## 주요 코드 경로 + +| 단계 | 코드 | +|------|------| +| 진입 | `app/services/message_service_v2.py:route_message()` | +| 의도분류 | `app/services/brain/intent_classifier.py:IntentClassifier.classify()` | +| 스킬 실행 | `app/services/skills/executor.py:execute()` | +| URL 해소 | `app/services/skills/registry.py:SkillMeta.resolve_url()` | +| 스킬 정의 | `DOCS/skills/skill-slack/SKILL.md` (트리거: `slack_thread`) | ## 엔드포인트 -- 아웃바운드: `POST http://192.168.219.52:8502/api/v1/summarize` -- 아웃바운드: Slack `chat.postMessage` (스레드 회신) + +- **아웃바운드**: `POST {SKILL_SLACK_URL}/api/v1/summarize` — skill-slack 서비스 +- `SKILL_SLACK_URL` 환경변수로 해소. IP 하드코딩 없음 ## 관련 문서 -- [slack_action_extractor_request](./slack_action_extractor_request.md) -- [slack_basic_dialogue](./slack_basic_dialogue.md) + +- [message_flow_v2.md](./message_flow_v2.md) — v2 메시지 흐름 +- [slack_action_extractor_request.md](./slack_action_extractor_request.md) — 액션 추출 (같은 skill-slack) +- [skill-slack SKILL.md](/home/admin/robeing/DOCS/skills/skill-slack/SKILL.md) — 스킬 계약 diff --git a/workflow/02_skills/skill_calendar_request.md b/workflow/02_skills/skill_calendar_request.md index 3309903..12e15c6 100644 --- a/workflow/02_skills/skill_calendar_request.md +++ b/workflow/02_skills/skill_calendar_request.md @@ -1,28 +1,64 @@ +--- +type: workflow +tags: [workflow, skill-calendar, calendar, executor, intent] +last_updated: 2026-04-06 +--- + # skill_calendar_request 워크플로우 ## 목적 -캘린더 스킬의 webhook 브릿지. 일정 생성(create) 또는 조회(query) 요청을 skill-calendar API로 전달하고 결과를 반환한다. + +캘린더 스킬 요청 처리. 일정 생성(create), 조회(list/get), 삭제(delete)를 SKILL.md 기반 executor를 통해 skill-calendar 서비스로 전달한다. + +## 현행 아키텍처 + +- **의도분류**: IntentClassifier가 SKILL.md 본문으로 `skill-calendar` 매칭 +- **실행**: executor가 `SKILL_CALENDAR_URL` 환경변수에서 URL 해소 후 HTTP 호출 +- **메서드 결정**: SKILL.md frontmatter `endpoints`가 action별 HTTP 메서드(GET/POST/DELETE)와 경로를 정의 + +n8n webhook 브릿지는 사용하지 않는다. ## 흐름 + ``` -Webhook In → Normalize Payload → Action Type → (create) Create Event → Return Result - → (else) Query Events → Return Result +사용자 발화 ("내일 2시 미팅 잡아줘") + -> IntentClassifier.classify() + -- IntentResult(skill="skill-calendar", action="create_event", endpoint="/api/events", slots={...}) + -> executor.execute() + -- registry.resolve_url() -> SKILL_CALENDAR_URL 환경변수 + -- registry.resolve_method("create_event") -> POST (SKILL.md frontmatter) + -- POST {SKILL_CALENDAR_URL}/api/events (httpx, X-User-Id 헤더) + -> _format_as_robeing() -> 응답 ``` -## 주요 노드 -| 노드 | 설명 | -|---|---| -| Webhook In | `POST /skills/calendar/request` 수신 | -| Normalize Payload | action(create/query), user_id, event_data, query 추출 | -| Action Type | action === "create" 분기 | -| Create Event | `POST :8512/api/events` (X-User-Id 헤더) | -| Query Events | `GET :8512/api/events?query=...` (X-User-Id 헤더) | -| Return Result | webhook 응답 반환 | +## SKILL.md 엔드포인트 정의 -## 엔드포인트 -- 인바운드: `POST /skills/calendar/request` (n8n webhook) -- 아웃바운드: `http://192.168.219.52:8512/api/events` (GET/POST) +| action | 메서드 | 경로 | +|--------|--------|------| +| `create_event` | POST | `/api/events` | +| `list_events` | GET | `/api/events` | +| `get_events` | GET | `/api/events` | +| `delete_event` | DELETE | `/api/events/{event_id}` | + +경로 파라미터(`{event_id}`)는 executor가 params에서 자동 보간한다. + +## 주요 코드 경로 + +| 단계 | 코드 | +|------|------| +| 스킬 정의 | `DOCS/skills/skill-calendar/SKILL.md` | +| 의도분류 | `app/services/brain/intent_classifier.py` | +| 실행 | `app/services/skills/executor.py:execute()` | +| URL/메서드 해소 | `app/services/skills/registry.py:SkillMeta` | + +## 환경변수 + +| 변수 | 용도 | +|------|------| +| `SKILL_CALENDAR_URL` | skill-calendar 서비스 베이스 URL | ## 관련 문서 -- [skill_email_send_request](./skill_email_send_request.md) -- [skill_news_briefing_request](./skill_news_briefing_request.md) + +- [skill-calendar SKILL.md](/home/admin/robeing/DOCS/skills/skill-calendar/SKILL.md) -- 스킬 계약 +- [skill_email_send_request.md](./skill_email_send_request.md) +- [message_flow_v2.md](../01_conversation/message_flow_v2.md) -- v2 메시지 흐름 diff --git a/workflow/02_skills/skill_email_send_request.md b/workflow/02_skills/skill_email_send_request.md index 704dc26..3960ed6 100644 --- a/workflow/02_skills/skill_email_send_request.md +++ b/workflow/02_skills/skill_email_send_request.md @@ -1,24 +1,69 @@ +--- +type: workflow +tags: [workflow, skill-email, email, executor, intent] +last_updated: 2026-04-06 +--- + # skill_email_send_request 워크플로우 ## 목적 -이메일 발송 스킬의 webhook 브릿지. 요청 payload를 그대로 skill-email API에 전달하고 결과를 반환한다. -## 흐름 +이메일 스킬 요청 처리. 이메일 발송(send), 조회(read), 요약(process)을 SKILL.md 기반 executor 또는 `email_integration` 모듈을 통해 skill-email 서비스로 전달한다. + +## 현행 아키텍처 + +- **v2 경로**: IntentClassifier -> executor -> `SKILL_EMAIL_URL` 환경변수에서 URL 해소 후 HTTP 호출 +- **Slack 경로**: `slack/message_service.py`에서 "네이버웍스 메일" 키워드 감지 시 `email_integration.send_naverworks_to_slack()` 직접 호출 +- **email_integration**: `app/services/skills/email_integration.py` -- Gmail/NaverWorks 멀티 프로바이더 지원 + +n8n webhook 브릿지는 사용하지 않는다. + +## 흐름 (v2 메시지 경로) + ``` -Webhook In → Call skill-email → Return Response +사용자 발화 ("메일 확인해줘") + -> IntentClassifier.classify() + -- IntentResult(skill="skill-email", action="email_read", endpoint="/messages", slots={...}) + -> executor.execute() + -- registry.resolve_url() -> SKILL_EMAIL_URL 환경변수 + -- registry.resolve_method("email_read") -> GET (SKILL.md frontmatter) + -- GET {SKILL_EMAIL_URL}/messages (httpx, X-User-Id 헤더) + -> _format_as_robeing() -> 응답 ``` -## 주요 노드 -| 노드 | 설명 | -|---|---| -| Webhook In | `POST /skills/email/send` 수신 | -| Call skill-email | `POST :8501/api/v1/send` (body 그대로 전달) | -| Return Response | webhook 응답 반환 | +## SKILL.md 엔드포인트 정의 -## 엔드포인트 -- 인바운드: `POST /skills/email/send` (n8n webhook) -- 아웃바운드: `POST http://192.168.219.52:8501/api/v1/send` +| action | 메서드 | 경로 | +|--------|--------|------| +| `send` / `email_send` | POST | `/send` | +| `read` / `email_read` | GET | `/messages` | +| `process` / `email_summary` | POST | `/process` | + +## email_integration 모듈 + +`app/services/skills/email_integration.py`의 `EmailIntegration` 클래스가 다음을 담당한다: + +| 기능 | 메서드 | +|------|--------| +| Gmail 장착 확인 | `check_gmail_equipped()` -- DB 직접 조회 | +| 프로바이더 결정 | `get_email_provider()` -- 키워드 또는 DB 조회 | +| 이메일 요청 처리 | `process_email_request()` -- skill-email HTTP 호출 | +| NaverWorks->Slack | `send_naverworks_to_slack()` -- 메일 조회 후 Slack 블록 전송 | +| UUID 매핑 | `get_uuid_from_slack()` -- AUTH_SERVER_URL 매핑 API | + +## 환경변수 + +| 변수 | 용도 | +|------|------| +| `SKILL_EMAIL_URL` | skill-email 서비스 베이스 URL | +| `SKILL_SLACK_URL` | Slack 전송 시 사용 | +| `AUTH_SERVER_URL` | Slack ID -> UUID 매핑 API | +| `INTERNAL_API_KEY` | skill-email 인증 | +| `SERVICE_API_KEY` | skill-slack 인증 | +| `POSTGRES_CONNECTION_STRING` | Gmail 토큰/프로바이더 DB 조회 | ## 관련 문서 -- [skill_calendar_request](./skill_calendar_request.md) -- [skill_slack_send_message_bridge](./skill_slack_send_message_bridge.md) + +- [skill-email SKILL.md](/home/admin/robeing/DOCS/skills/skill-email/SKILL.md) -- 스킬 계약 +- [skill_calendar_request.md](./skill_calendar_request.md) +- [message_flow_v2.md](../01_conversation/message_flow_v2.md) -- v2 메시지 흐름 diff --git a/workflow/02_skills/skill_news_briefing_request.md b/workflow/02_skills/skill_news_briefing_request.md index 56b7614..1c2e4d7 100644 --- a/workflow/02_skills/skill_news_briefing_request.md +++ b/workflow/02_skills/skill_news_briefing_request.md @@ -1,24 +1,61 @@ +--- +type: workflow +tags: [workflow, skill-news, news, executor, intent] +last_updated: 2026-04-06 +--- + # skill_news_briefing_request 워크플로우 ## 목적 -뉴스 브리핑 스킬의 webhook 브릿지. 요청 payload를 그대로 skill-news API에 전달하고 결과를 반환한다. + +뉴스 스킬 요청 처리. 뉴스 검색(search), 요약(summarize), 최신 뉴스 조회(latest)를 SKILL.md 기반 executor를 통해 skill-news 서비스로 전달한다. + +## 현행 아키텍처 + +- **의도분류**: IntentClassifier가 SKILL.md 본문으로 `skill-news` 매칭 +- **실행**: executor가 `SKILL_NEWS_URL` 환경변수에서 URL 해소 후 HTTP 호출 + +n8n webhook 브릿지는 사용하지 않는다. ## 흐름 + ``` -Webhook In → Call skill-news → Return Response +사용자 발화 ("요즘 뉴스 있어?") + -> IntentClassifier.classify() + -- IntentResult(skill="skill-news", action="news_fetch", slots={...}) + -> executor.execute() + -- registry.resolve_url() -> SKILL_NEWS_URL 환경변수 + -- POST {SKILL_NEWS_URL}/api/news/search (httpx) + -> _format_as_robeing() -> 응답 ``` -## 주요 노드 -| 노드 | 설명 | -|---|---| -| Webhook In | `POST /skills/news/briefing` 수신 | -| Call skill-news | `POST :8505/api/v1/briefing` (body 그대로 전달) | -| Return Response | webhook 응답 반환 | +## SKILL.md 엔드포인트 -## 엔드포인트 -- 인바운드: `POST /skills/news/briefing` (n8n webhook) -- 아웃바운드: `POST http://192.168.219.52:8505/api/v1/briefing` +| action | 메서드 | 경로 | 설명 | +|--------|--------|------|------| +| `news_fetch` | POST | `/api/news/search` | 키워드 기반 뉴스 검색 | +| `news_summary` | POST | `/api/news/summarize` | 기사 요약 | +| (조회) | GET | `/api/news/latest` | 최신 뉴스 목록 | + +## 주요 코드 경로 + +| 단계 | 코드 | +|------|------| +| 스킬 정의 | `DOCS/skills/skill-news/SKILL.md` | +| 의도분류 | `app/services/brain/intent_classifier.py` | +| 실행 | `app/services/skills/executor.py:execute()` | +| 뉴스 게시 | `app/services/skills/news_posting_skill.py` -- Slack 채널 뉴스 게시 | +| 스타트업 뉴스 | `app/services/skills/startup_news_skill.py` | +| 헤드라인 워크플로우 | `app/services/workflows/headlines_workflow.py` -- 일일 헤드라인 | + +## 환경변수 + +| 변수 | 용도 | +|------|------| +| `SKILL_NEWS_URL` | skill-news 서비스 베이스 URL | ## 관련 문서 -- [skill_calendar_request](./skill_calendar_request.md) -- [scheduled_daily_briefing](../04_scheduler/scheduled_daily_briefing.md) + +- [skill-news SKILL.md](/home/admin/robeing/DOCS/skills/skill-news/SKILL.md) -- 스킬 계약 +- [skill_calendar_request.md](./skill_calendar_request.md) +- [message_flow_v2.md](../01_conversation/message_flow_v2.md) -- v2 메시지 흐름 diff --git a/workflow/02_skills/skill_slack_send_message_bridge.md b/workflow/02_skills/skill_slack_send_message_bridge.md index e7b6542..9ea9979 100644 --- a/workflow/02_skills/skill_slack_send_message_bridge.md +++ b/workflow/02_skills/skill_slack_send_message_bridge.md @@ -1,25 +1,65 @@ +--- +type: workflow +tags: [workflow, skill-slack, slack, send-message, slack-sdk] +last_updated: 2026-04-06 +--- + # skill_slack_send_message_bridge 워크플로우 ## 목적 -Slack 메시지 발송 브릿지. 내부 서비스가 Slack 메시지를 보내야 할 때 이 webhook을 통해 n8n Slack 노드로 발송한다. -## 흐름 +내부 서비스가 Slack 메시지를 보내야 할 때의 경로. 현행에서는 `slack_sdk.WebClient`로 직접 Slack API를 호출하거나, skill-slack 서비스를 HTTP로 호출한다. + +## 현행 아키텍처 + +n8n Slack 노드 경유는 사용하지 않는다. 두 가지 경로가 존재한다: + +### 경로 1: slack_sdk.WebClient 직접 호출 (Slack ���벤트 응답) + +`app/services/slack/message_service.py`에서 `WebClient(token=bot_token)`으로 Slack API를 직접 호출한다. + ``` -Webhook In → Normalize Payload → Post to Slack → Return Response +slack_endpoint.py (events 수신) + -> slack_handler.py (이벤트 처리) + -> slack/message_service.py:process_slack_message() + -- WebClient(token=bot_token) + -- local_client.chat_postMessage(channel, text, thread_ts, blocks) ``` -## 주요 노드 -| 노드 | 설명 | -|---|---| -| Webhook In | `POST /skills/slack/send` 수신 | -| Normalize Payload | channel, text, thread_ts, blocks 추출 | -| Post to Slack | Slack `chat.postMessage` (스레드 지원) | -| Return Response | `{success, channel, thread_ts}` 반환 | +봇 토큰은 Slack 이벤트 컨텍스트에서 전달받는다. -## 엔드포인트 -- 인바운드: `POST /skills/slack/send` (n8n webhook) -- 아웃바운드: Slack `chat.postMessage` +### 경로 2: skill-slack HTTP 호출 (스케줄러/워크플로우) + +스케줄러 잡이나 LangGraph 워크플로우에서 Slack 메시지를 보낼 때��� skill-slack 서비스를 HTTP로 호출한다. + +``` +coldmail_workflow.py / headlines_workflow.py + -> POST {SKILL_SLACK_URL}/api/v1/send + headers: X-API-Key + body: { channel, text, blocks, thread_ts } +``` + +## 주요 코드 경로 + +| 용도 | 코드 | Slack 호출 ���식 | +|------|------|----------------| +| Slack 이벤트 응답 | `app/services/slack/message_service.py` | `slack_sdk.WebClient` 직접 | +| 콜드메일 알림 | `app/services/workflows/coldmail_workflow.py:send_node()` | skill-slack HTTP | +| 헤드라인 게시 | `app/services/workflows/headlines_workflow.py` | skill-slack HTTP | +| 뉴스 게시 | `app/services/skills/news_posting_skill.py` | `slack_sdk.WebClient` 직접 | +| DM 발송 | `app/services/skills/dm_skill.py` | `slack_sdk.WebClient` 직접 | +| 명확화 UI | `app/services/slack/clarify_service.py` | `slack_sdk.WebClient` 직접 | + +## 환경변수 + +| 변수 | 용도 | +|------|------| +| `SKILL_SLACK_URL` | skill-slack 서비스 베이스 URL (HTTP 경로) | +| `SKILL_SLACK_API_KEY` / `SERVICE_API_KEY` | skill-slack 인증 키 | +| `SLACK_BOT_TOKEN` | 기본 워크스페이스 봇 토큰 (직접 호출 시) | ## 관련 문서 -- [skill_email_send_request](./skill_email_send_request.md) -- [slack_basic_dialogue](../01_conversation/slack_basic_dialogue.md) + +- [slack_direct_api_send.md](./slack_direct_api_send.md) -- DB 봇토큰 직접 전송 경로 +- [skill-slack SKILL.md](/home/admin/robeing/DOCS/skills/skill-slack/SKILL.md) -- 스킬 계약 +- [message_flow_v2.md](../01_conversation/message_flow_v2.md) -- v2 메시지 흐름 diff --git a/workflow/02_skills/slack_direct_api_send.md b/workflow/02_skills/slack_direct_api_send.md index 1c645cf..005fd1f 100644 --- a/workflow/02_skills/slack_direct_api_send.md +++ b/workflow/02_skills/slack_direct_api_send.md @@ -1,24 +1,30 @@ +--- +type: workflow +tags: [workflow, slack, direct-api, bot-token, grounding] +last_updated: 2026-04-06 +--- + # Slack Direct API 메시지 전송 워크플로우 ## 목적 -에이전트(23 클로드 등)가 Slack API를 직접 호출하여 특정 워크스페이스 채널에 메시지를 보내는 경로. -n8n 브릿지(`skill_slack_send_message_bridge`)와 별개로, DB에 저장된 봇 토큰을 사용하여 직접 전송한다. +에이전��(23 클로드 등)가 Slack API를 직접 호출하여 특정 워크스페이스 채널에 메시지를 보내는 경로. +DB에 저장된 봇 토큰을 사용하여 직접 전송한다. ## 실측 경로 (260323 검증) ``` 1. DB에서 봇 토큰 조회 - └─ slack_workspace 테이블: slack_team_id → bot_token + -- slack_workspace 테이블: slack_team_id -> bot_token 2. Slack API 호출 - └─ POST https://slack.com/api/chat.postMessage + -- POST https://slack.com/api/chat.postMessage Headers: Authorization: Bearer {bot_token} Body: { channel, text } 3. 응답 확인 - └─ ok: true → 전송 성공 - └─ ok: false → error 필드 확인 + -- ok: true -> 전송 성공 + -- ok: false -> error 필드 확인 ``` ## 워크스페이스별 봇 토큰 조회 @@ -33,23 +39,11 @@ FROM slack_workspace; | 디지털비징_로빙 | T0925SXPS4D | 내부 테스트 | | 컴퍼니엑스 | T09C98KB933 | 실서비스 (Company X) | -## 채널 ID 확인 - -```bash -# 봇이 접근 가능한 DM 목록 -curl -s -H "Authorization: Bearer {bot_token}" \ - "https://slack.com/api/conversations.list?types=im,mpim&limit=50" - -# 특정 채널 최근 메시지 -curl -s -H "Authorization: Bearer {bot_token}" \ - "https://slack.com/api/conversations.history?channel={channel_id}&limit=5" -``` - ## 메시지 전송 ```bash curl -s -X POST \ - -H "Authorization: Bearer {bot_token}" \ + -H "Authorization: Bearer $BOT_TOKEN" \ -H "Content-Type: application/json; charset=utf-8" \ -d '{ "channel": "{channel_id}", @@ -58,29 +52,32 @@ curl -s -X POST \ "https://slack.com/api/chat.postMessage" ``` +봇 토큰은 DB(`slack_workspace` 테이블)에서 조회한다. 환경변수 또는 코드에 하드코딩하지 않는다. + ## 주의사항 -1. **봇 토큰으로 보내면 발신자가 Robeing** — 봇이 자기 자신에게 보내는 꼴이라 로빙이 응답하지 않음 +1. **봇 토큰으로 ��내면 발신자가 Robeing** -- 봇이 자기 자신에게 보내는 꼴이라 로빙이 응답하지 않음 2. **사용자에게 메시지를 보내려면** 사용자의 user_id로 `conversations.open` 후 DM 채널 생성 필요 -3. **워크스페이스가 다르면 토큰도 다름** — 반드시 대상 워크스페이스의 bot_token 사용 -4. **channel_not_found 에러** — 봇이 해당 채널에 접근 권한이 없거나, 다른 워크스페이스의 채널 ID를 사용한 경우 +3. **워크스페이스가 다르면 토큰도 다름** -- 반드시 대상 워크스페이스의 bot_token 사용 +4. **channel_not_found 에러** -- 봇이 해당 채널에 접근 권한이 없거나, 다른 워크스페이스의 채널 ID를 사용한 경우 ## 260323 실측 결과 | 시도 | 워크스페이스 | 채널 | 결과 | |------|-------------|------|------| | 1차 | 디지털비징_로빙 (T0925SXPS4D) | D09DWLAV88Y | channel_not_found (워크스페이스 불일치) | -| 2차 | 디지털비징_로빙 | D0935RJ8LLQ | 전송 성공 (CTO DM) — 단, 로빙 응답 없음 (봇→봇) | -| 3차 | 컴퍼니엑스 (T09C98KB933) | D09DWLAV88Y | 전송 성공 — 로빙 응답함 (grounding 질문만 답, 나머지 무시) | +| 2차 | 디지털비징_로빙 | D0935RJ8LLQ | 전송 성공 (CTO DM) -- 단, 로빙 응답 없음 (봇->봇) | +| 3차 | 컴퍼니엑스 (T09C98KB933) | D09DWLAV88Y | 전송 성공 -- 로빙 응답함 (grounding 질문만 답, 나머지 무시) | -## 로빙 응답 분석 (260323) +## 로빙 응답 분석 (260323) 및 이후 개선 -질문 6개 중 "컴퍼니엑스가 뭐하는 회사야?"만 답변. 나머지 5개(시간/날짜/이름/산수/대화이력) 전부 무시. -- 원인: "컴퍼니엑스" 마커에 grounding이 트리거되면 RAG 결과만 반환하고 나머지 질문 드랍 -- 참고: `[24]_260323_로빙교육_못하는것3가지.md`, `[24]_260323_로빙교육_못하는것_2차진단.md` +질문 6개 중 "컴퍼니엑스가 뭐하�� 회사야?"만 답변. 나머지 5개(시간/날짜/이름/산수/대화이력) 전부 무시. + +- **원인**: "컴퍼니엑스" 마커에 grounding이 트리거되면 RAG 결과만 반환하고 나머지 질문 드랍 +- **현행 개선**: `companyx_grounding_service.py`에서 `should_handle_companyx_grounding()` 함수가 LLM이 명확한 의도를 이미 확정한 경우 grounding이 덮어쓰지 않도록 개선됨. `_looks_like_companyx_grounding_question()` 함수가 grounding 대상 여부를 사전 판별한다. +- **team_id 기반 ���기**: Slack team_id로 워크스페이스를 식별하고, DB team UUID로 매핑한 후 grounding/RAG 검색에 활용 ## 관련 문서 -- [skill_slack_send_message_bridge](./skill_slack_send_message_bridge.md) — n8n 브릿지 경로 -- [slack_basic_dialogue](../01_conversation/slack_basic_dialogue.md) — 슬랙 대화 워크플로우 -- [companyx_grounding_pipeline](../03_rag/companyx_grounding_pipeline.md) — grounding 라우팅 +- [skill_slack_send_message_bridge.md](./skill_slack_send_message_bridge.md) -- skill-slack HTTP/직접 ���송 경로 +- [companyx_grounding_pipeline.md](../03_rag/companyx_grounding_pipeline.md) -- grounding 라우팅