docs: 완전 구현된 plan 문서 삭제 (troubleshooting 이동 완료)
This commit is contained in:
parent
a6e4d9b3ab
commit
4c33743d9f
@ -1,194 +0,0 @@
|
||||
# Company-X 뉴스 시스템 구현 완료
|
||||
|
||||
## 작성일: 2025-09-07 (최종 업데이트: 2025-09-08)
|
||||
## 작성자: Claude & happybell80
|
||||
## 목적: Company-X 뉴스 포스팅 시스템 구현 내역 기록
|
||||
|
||||
## 1. 구현 완료 사항
|
||||
|
||||
### 1.1 Slack Interactive 시스템
|
||||
- news_posting_skill.py: Company-X 뉴스 채널 메시지 생성 및 skill-slack 전송 요청 기능
|
||||
- Block Kit 메시지 형식 (버튼 포함)
|
||||
- 홈페이지 게시 / 건너뛰기 버튼
|
||||
- skill-publish 연동 코드
|
||||
- JSON 기반 상태 관리
|
||||
|
||||
### 1.2 Interactive Endpoint
|
||||
- main.py: /api/slack/interactive 엔드포인트 추가
|
||||
- slack_handler.py: handle_interactive() 함수 구현
|
||||
- Slack 서명 검증
|
||||
- 버튼 액션 처리
|
||||
- 3초 내 ACK 응답
|
||||
|
||||
### 1.3 환경 설정
|
||||
- 채널 ID 관리: 환경변수 방식 채택
|
||||
- COMPANY_X_NEWS_CHANNEL_ID=C09CP4MDX71
|
||||
- /home/heejae/rb8001/.env: 환경변수 추가
|
||||
- /home/admin/ivada_project/rb8001/.env: 동일 설정 동기화
|
||||
|
||||
### 1.4 중복 뉴스 방지 시스템
|
||||
- sent_news_tracker.py: 전송된 뉴스 추적
|
||||
- 30일 히스토리 유지
|
||||
- JSON 파일 기반 저장
|
||||
- 사용자별 중복 필터링
|
||||
- Dockerfile 수정: sent_news_tracker.py 포함
|
||||
|
||||
## 2. 해결한 문제들
|
||||
|
||||
### 2.1 sent_news_tracker 모듈 에러
|
||||
- 문제: No module named 'sent_news_tracker'
|
||||
- 원인: Docker 이미지에 파일 미포함
|
||||
- 해결: Dockerfile에 COPY 명령 추가
|
||||
|
||||
### 2.2 채널 ID 하드코딩 이슈
|
||||
- 문제: 채널 ID가 코드에 하드코딩됨
|
||||
- 초기 설계: DB 테이블 방식
|
||||
- 최종 결정: 환경변수 방식
|
||||
|
||||
### 2.3 Git 충돌 해결
|
||||
- 충돌 파일: news_posting_skill.py, slack_handler.py, main.py
|
||||
- 해결 방식: 기존 코드 최대한 보존하며 새 기능 통합
|
||||
|
||||
## 3. 테스트 방법
|
||||
|
||||
### 수동 테스트
|
||||
- Company-X 뉴스 즉시 실행: curl -X POST http://localhost:8001/api/test/companyx-news
|
||||
- 로그 확인: docker logs rb8001
|
||||
|
||||
## 4. 환경 설정 요약
|
||||
|
||||
### 필수 환경변수
|
||||
- SLACK_BOT_TOKEN
|
||||
- SLACK_SIGNING_SECRET
|
||||
- COMPANY_X_NEWS_CHANNEL_ID=C09CP4MDX71
|
||||
- SKILL_NEWS_URL=http://localhost:8505
|
||||
- SKILL_PUBLISH_URL=http://localhost:8511
|
||||
|
||||
## 5. 주의사항
|
||||
- Docker 재시작시: docker compose down && up 사용
|
||||
- 환경변수 변경시 두 곳 모두 수정 필요
|
||||
- 운영 전환시 COMPANY_X_NEWS_CHANNEL_ID만 변경
|
||||
|
||||
## 6. 향후 개선 사항
|
||||
- Slack App Interactive URL 실제 설정
|
||||
- skill-email 500 에러 해결
|
||||
- 운영 채널 전환 테스트
|
||||
- 게시 완료 후 메시지 업데이트 기능
|
||||
|
||||
## 7. 2025-09-08 업데이트
|
||||
|
||||
### 7.1 구현 완료
|
||||
- Gateway /slack/interactive 프록시: 헤더 소문자 변환 문제 해결 (x-slack-signature)
|
||||
- skill-news GET /api/news/google/companyx/{article_id} 엔드포인트 추가
|
||||
- skill-news PATCH /api/news/google/companyx/{article_id} 상태 업데이트 추가
|
||||
- rb8001 news_id → article_id 변경
|
||||
- rb8001 skill-news에서 전체 데이터 조회 후 skill-publish 전송
|
||||
- 강원일보 스크래핑: div[itemprop='articleBody'] 셀렉터 추가
|
||||
- 강원도민일보 스크래핑: #article-view-content-div 셀렉터 추가
|
||||
- 날짜 추출: HTML 전체에서 "입력 2025.09.02" 패턴 검색 추가
|
||||
- parsed_date 필드 스크래핑 후 업데이트
|
||||
- Slack 메시지: 3개 제한 제거, 모든 뉴스 표시
|
||||
- 버튼 상태: 에러시에도 버튼 유지, 성공시 상태 텍스트만 추가
|
||||
|
||||
## 8. 2025-09-08 주요 문제 해결
|
||||
|
||||
### 8.1 Slack 메시지 전체 사라짐 문제
|
||||
#### 증상
|
||||
- 홈페이지 게시 버튼 클릭시 원본 메시지 전체가 사라지고 텍스트만 남음
|
||||
- "(편집됨)" 표시와 함께 버튼과 뉴스 내용이 모두 제거됨
|
||||
|
||||
#### 원인
|
||||
1. Slack Interactive 응답에 JSON 메시지 포함시 원본 교체로 해석
|
||||
2. response_url 사용시 replace_original 기본값이 true일 수 있음
|
||||
3. chat_update() 호출이 blocks 전체를 잘못 전달
|
||||
|
||||
#### 해결
|
||||
- rb8001/app/router/slack_handler.py Line 342: 빈 JSONResponse({}) 반환
|
||||
- rb8001/app/router/slack_handler.py Line 396-397: replace_original=False, delete_original=False 명시
|
||||
- chat_update() 호출 전면 제거
|
||||
|
||||
### 8.2 skill-publish 422 에러
|
||||
#### 증상
|
||||
- rb8001이 skill-publish 호출시 422 Unprocessable Entity 에러
|
||||
|
||||
#### 원인
|
||||
- rb8001이 news_id, title, url만 전송
|
||||
- content, summary, thumbnail_url 필수 필드 누락
|
||||
|
||||
#### 해결
|
||||
- rb8001/app/skills/news_posting_skill.py Line 282-285: skill-news에서 전체 데이터 조회
|
||||
- skill-news/app/api/news_endpoints.py Line 149-160: GET /api/news/google/companyx/{article_id} 추가
|
||||
- skill-news/app/api/news_endpoints.py Line 163-181: PATCH /api/news/google/companyx/{article_id} 추가
|
||||
|
||||
### 8.3 Gateway /slack/interactive 프록시 실패
|
||||
#### 증상
|
||||
- Slack Interactive 이벤트가 rb8001에 도달하지 않음
|
||||
|
||||
#### 원인
|
||||
- robeing-gateway에 /slack/interactive 엔드포인트 없음
|
||||
- FastAPI/Starlette가 헤더를 소문자로 변환 (X-Slack-Signature → x-slack-signature)
|
||||
|
||||
#### 해결
|
||||
- robeing-gateway/app/main.py Line 239-283: /slack/interactive 프록시 엔드포인트 추가
|
||||
- Line 254-257: 헤더 소문자로 접근 (request.headers.get("x-slack-signature"))
|
||||
|
||||
### 8.4 article_id None 문제
|
||||
#### 증상
|
||||
- skill-news GET /api/news/google/companyx/None 404 에러
|
||||
|
||||
#### 원인
|
||||
- 버튼 value에서 article_id 추출 실패
|
||||
- news_posting_skill.py Line 273: value_str이 이미 string인데 json.loads() 실패
|
||||
|
||||
#### 해결
|
||||
- rb8001/app/skills/news_posting_skill.py Line 107: article_id 사용
|
||||
- Line 150: action_data에 article_id 포함
|
||||
- Line 273: isinstance(value_str, str) 체크 추가
|
||||
|
||||
### 8.5 강원일보/강원도민일보 스크래핑 실패
|
||||
#### 증상
|
||||
- 본문 0자, 날짜 추출 실패
|
||||
|
||||
#### 원인
|
||||
- 기존 article/main/p 태그 셀렉터로 찾을 수 없음
|
||||
- 사이트별 특수한 HTML 구조 사용
|
||||
|
||||
#### 해결
|
||||
- skill-news/app/services/companyx_news_scraper.py Line 159-160: 특수 셀렉터 추가
|
||||
- 강원일보: div[itemprop='articleBody']
|
||||
- 강원도민일보: #article-view-content-div
|
||||
- Line 266-277: HTML 전체에서 날짜 패턴 검색 추가
|
||||
|
||||
### 8.6 날짜 표시 오류
|
||||
#### 증상
|
||||
- Slack에 "강원도민일보 | 2025-09-08" (실제 기사는 2025-09-02)
|
||||
|
||||
#### 원인
|
||||
1. Google News에서 "3일 전" 같은 상대 시간만 제공
|
||||
2. 스크래핑 후 실제 날짜 추출했지만 parsed_date 필드 업데이트 안 함
|
||||
|
||||
#### 해결
|
||||
- skill-news/app/services/companyx_news_scraper.py Line 106: parsed_date 전달
|
||||
- skill-news/app/services/companyx_news_collector.py Line 364: parsed_date 저장
|
||||
|
||||
### 8.7 Slack 요약 200자 제한
|
||||
#### 증상
|
||||
- 요약이 "..." 으로 끝남
|
||||
|
||||
#### 원인
|
||||
- rb8001/app/skills/news_posting_skill.py Line 110-111: 200자 초과시 197자로 자름
|
||||
|
||||
#### 현재 상태
|
||||
- 수정하지 않음 (가독성 위해 유지)
|
||||
|
||||
## 9. 핵심 교훈
|
||||
|
||||
### 9.1 Slack Interactive 응답 규칙
|
||||
- 3초 내 빈 200 OK 응답 필수
|
||||
- JSON 메시지 반환시 원본 교체됨
|
||||
- response_url 사용시 replace_original=False 명시 필수
|
||||
|
||||
### 9.2 데이터 흐름
|
||||
- skill-news를 single source of truth로 사용
|
||||
- rb8001은 orchestrator 역할만
|
||||
- 모든 데이터는 skill-news에서 관리
|
||||
@ -1,264 +0,0 @@
|
||||
# skill-email 서비스 NAVER WORKS 확장 계획
|
||||
|
||||
## 날짜: 2025-09-18
|
||||
## 작성자: Claude (51123 서버 관리자)
|
||||
## 상태: ✅ 구현 완료 (2025-09-19)
|
||||
## 서비스: skill-email (51124 서버, 포트 8501)
|
||||
|
||||
---
|
||||
|
||||
## 1. 목표
|
||||
|
||||
기존 Gmail 전용 skill-email 서비스를 멀티 프로바이더 이메일 서비스로 확장하여 NAVER WORKS Mail API를 지원
|
||||
|
||||
### 주요 목표
|
||||
- ✅ 기존 Gmail 기능 유지
|
||||
- 🎯 NAVER WORKS 메일 조회/발송 추가
|
||||
- 🎯 Provider 파라미터로 서비스 구분
|
||||
- 🎯 Slack 통합 지원
|
||||
|
||||
---
|
||||
|
||||
## 2. 현재 구조 분석
|
||||
|
||||
### 2.1 skill-email 현황 (확인 완료)
|
||||
```
|
||||
위치: 51124 서버
|
||||
포트: 8501
|
||||
기능: Gmail 전용 메일 발송/조회
|
||||
인증: gmail_token 테이블 (user.oauth_id JOIN 필요)
|
||||
엔드포인트: /send, /messages, /register_watch, /push, /process, /health
|
||||
구조: services/gmail_service.py, db_credentials_provider.py, api_credentials_provider.py
|
||||
특징:
|
||||
- Google 라이브러리가 직접 refresh 처리 (auth-server 호출 안함)
|
||||
- 401 에러 시 자동 refresh 후 재시도
|
||||
- robeing-monitor 경유 (X-User-Id 헤더 사용)
|
||||
```
|
||||
|
||||
### 2.2 NAVER WORKS 준비 현황 (구현 완료)
|
||||
- ✅ OAuth 인증 구현 완료 (auth-server)
|
||||
- ✅ naverworks_token 테이블 구조 완료
|
||||
- ✅ Mail API 올바른 엔드포인트 확인 완료 (/mail/mailfolders/{folderId}/children)
|
||||
- ✅ 토큰 갱신 로직 구현 완료 (/auth/naverworks/passport/refresh)
|
||||
|
||||
---
|
||||
|
||||
## 3. 확장 설계
|
||||
|
||||
### 3.1 Provider 추상화 (✅ 구현 완료)
|
||||
- **구현 내용**:
|
||||
- typing.Protocol로 EmailProvider 인터페이스 정의
|
||||
- ProviderRegistry 클래스로 Provider 관리
|
||||
- GmailProvider: 기존 GmailService 래핑
|
||||
- NaverWorksProvider: NAVER WORKS Mail API 구현
|
||||
- 파일: services/email_provider.py, gmail_provider.py, naverworks_provider.py
|
||||
|
||||
### 3.2 API 엔드포인트 수정 (✅ 구현 완료)
|
||||
- **구현 내용**:
|
||||
- `/send`: provider 파라미터 추가 (기본값: gmail)
|
||||
- `/messages`: provider 파라미터 추가 (기본값: gmail)
|
||||
- 역호환성 100% 유지 (기존 API 그대로 작동)
|
||||
- main.py에서 get_email_provider() 함수로 Provider 선택
|
||||
|
||||
### 3.3 토큰 관리 통합 (✅ 구현 완료)
|
||||
- **수정 완료**:
|
||||
- DBCredentialsProvider: user 테이블 JOIN 방식으로 변경
|
||||
- 모든 쿼리: `JOIN "user" u ON gt.user_id = u.id WHERE u.oauth_id = %s`
|
||||
- token_data JSONB 통일: 상위 컬럼 사용 안함
|
||||
- APICredentialsProvider: 하드코딩 OAuth 설정 제거
|
||||
- **토큰 갱신 방식**:
|
||||
- Gmail: Google 라이브러리가 자동 refresh
|
||||
- NaverWorks: auth-server `/auth/naverworks/passport/refresh` 호출
|
||||
- DB 트랜잭션으로 안전한 rotation 처리
|
||||
|
||||
---
|
||||
|
||||
## 4. 구현 완료 (2025-09-19 ~ 2025-09-20)
|
||||
|
||||
### Phase 1: 기반 구조 ✅
|
||||
- [x] Provider 인터페이스를 async로 변경 (EmailProvider Protocol)
|
||||
- [x] 기존 Gmail 코드를 GmailProvider로 리팩토링 (run_in_executor 사용)
|
||||
- [x] NaverWorksProvider async 구현
|
||||
|
||||
### Phase 2: NAVER WORKS API 구현 ✅
|
||||
- [x] NaverWorksProvider 클래스 생성
|
||||
- [x] Mail API URL 패턴 수정
|
||||
- [x] 메일 목록: `/users/{userId}/mail/mailfolders/0/children`
|
||||
- [x] 메일 발송: `POST /users/{userId}/mail`
|
||||
- [x] 메일 상세: `GET /users/{userId}/mail/{mailId}` (응답 구조 {"mail": {...}})
|
||||
- [x] 메일 삭제: `POST /users/{userId}/mail/{mailId}/trash`
|
||||
- [x] 토큰 갱신 로직 (auth-server 연동)
|
||||
|
||||
### Phase 3: asyncio 이벤트 루프 문제 해결 ✅
|
||||
- [x] NaverWorksProvider의 asyncio.run() 제거 (FastAPI 충돌 해결)
|
||||
- [x] 모든 Provider 메서드를 async로 통일
|
||||
- [x] main.py에서 await 추가
|
||||
|
||||
### Phase 4: AccountContext 패턴 도입 ✅
|
||||
- [x] AccountContext dataclass 정의 (토큰 + account_id)
|
||||
- [x] _get_account_context() 메서드로 DB 조회 통합
|
||||
- [x] 한 번의 쿼리로 token_data와 account_id 조회
|
||||
- [x] account_id NULL 시 DEFAULT_USER_ID 폴백
|
||||
- [x] 기존 _get_access_token() 유지 (하위 호환성)
|
||||
|
||||
### Phase 5: 테스트 및 검증 ✅
|
||||
- [x] 토큰 갱신 테스트 성공
|
||||
- [x] NaverWorks 메일 목록 조회 성공
|
||||
- [x] NaverWorks 메일 상세 조회 성공 (응답 구조 수정)
|
||||
- [x] NaverWorks get_message body 처리 개선 (문자열/dict 타입 대응)
|
||||
- [x] Git push 및 Actions 배포 완료
|
||||
|
||||
---
|
||||
|
||||
## 5. 주요 기능
|
||||
|
||||
### 5.1 메일 요약 (Slack 전용)
|
||||
- 최근 메일 5-10개 조회
|
||||
- Gemini API로 요약 생성
|
||||
- Slack 블록 포맷으로 응답
|
||||
|
||||
### 5.2 프로바이더 상태 확인
|
||||
- Gmail 토큰 상태
|
||||
- NAVER WORKS 토큰 상태
|
||||
- 사용자 기본 프로바이더 설정
|
||||
|
||||
---
|
||||
|
||||
## 6. 데이터베이스 스키마
|
||||
|
||||
### 6.1 기존 테이블 (변경 없음)
|
||||
- `gmail_token`: Gmail OAuth 토큰
|
||||
- `naverworks_token`: NAVER WORKS OAuth 토큰
|
||||
|
||||
### 6.2 신규 테이블 (선택적)
|
||||
```sql
|
||||
-- 사용자 기본 이메일 프로바이더 설정
|
||||
CREATE TABLE email_provider_preference (
|
||||
user_id UUID PRIMARY KEY,
|
||||
default_provider VARCHAR(20) DEFAULT 'gmail',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Slack → NAVER WORKS 사용자 매핑
|
||||
CREATE TABLE slack_naverworks_mapping (
|
||||
slack_user_id VARCHAR PRIMARY KEY,
|
||||
naverworks_user_id UUID,
|
||||
email VARCHAR,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. API 명세
|
||||
|
||||
### 7.1 메일 발송
|
||||
```http
|
||||
POST /send
|
||||
{
|
||||
"provider": "naverworks",
|
||||
"user_id": "3550cef6-63e1-4ceb-8802-a25c9d1c6917",
|
||||
"to": "recipient@example.com",
|
||||
"subject": "회의 일정 안내",
|
||||
"body": "내일 오후 2시 회의입니다."
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 메일 조회
|
||||
```http
|
||||
GET /messages?provider=naverworks&user_id={user_id}&limit=10
|
||||
```
|
||||
|
||||
### 7.3 메일 요약 (Slack용) - 미구현
|
||||
```http
|
||||
POST /mail-summary
|
||||
{
|
||||
"provider": "naverworks",
|
||||
"user_id": "3550cef6-63e1-4ceb-8802-a25c9d1c6917",
|
||||
"slack_channel": "C1234567890"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 토큰 갱신 구현 위치 및 정책
|
||||
|
||||
### 8.1 NAVER WORKS Refresh Token Rotation 정책 (확인 완료)
|
||||
- **핵심**: refresh_token 사용 시 새로운 access_token + 새로운 refresh_token 발급
|
||||
- **기존 refresh_token 즉시 무효화** (재사용 절대 불가)
|
||||
- **수명**: Access Token 1시간, Refresh Token 90일
|
||||
- **위험**: DB 저장 실패 시 영구 토큰 손실, 재인증 필요
|
||||
|
||||
### 8.2 구현 위치 (✅ 구현 완료)
|
||||
**auth-server에서 중앙 관리**
|
||||
- 위치: `/home/admin/auth-server/app/providers/naverworks_passport.py`
|
||||
- 포트: 9000
|
||||
- 엔드포인트: `/auth/naverworks/passport/refresh`
|
||||
- DB 트랜잭션 + FOR UPDATE 행 잠금으로 안전 처리
|
||||
|
||||
### 8.3 현재 인프라 상태 (확인 완료)
|
||||
- Redis 컨테이너: 실행 중
|
||||
- auth-server 컨테이너: 실행 중
|
||||
- **DB 스키마 확인**:
|
||||
- gmail_token: token_data (JSONB)에 access_token, refresh_token 저장
|
||||
- naverworks_token: token_data (JSONB), oauth_config (JSONB) 사용
|
||||
- oauth_config: client_id, client_secret, token_uri 저장
|
||||
- 만료 필드: `expiry` (TIMESTAMP) 사용, `expires_at` (Float)는 레거시
|
||||
- user.oauth_id: Slack(U로 시작), NaverWorks(UUID 형식) 저장
|
||||
- **주의**: slack_user_id 컬럼 없음, user.oauth_id JOIN 필요
|
||||
|
||||
### 8.4 동시성 제어 (결정 완료)
|
||||
- **현재**: Redis 클라이언트만 있고 Lock 구현 없음
|
||||
- **결정**: DB 트랜잭션 + 낙관적 락 사용
|
||||
- asyncpg 트랜잭션으로 원자적 처리
|
||||
- version 필드 추가로 경쟁 조건 방지
|
||||
- 토큰 rotation 전체 과정 (조회→검증→저장→상태 갱신)을 단일 트랜잭션으로 처리
|
||||
|
||||
---
|
||||
|
||||
## 9. 구현 시 주의사항
|
||||
|
||||
### 9.1 확인된 이슈
|
||||
- **DB 스키마 불일치**: skill-email 코드가 없는 컬럼(slack_user_id) 참조
|
||||
- 참고: `/troubleshooting/250918_gmail_token_slack_user_id_column_missing.md`
|
||||
- 해결: user 테이블과 JOIN 필요 (u.oauth_id = slack_id)
|
||||
- **토큰 영구 손실 위험**: refresh_token rotation 실패 시 재인증 필요
|
||||
- **동시 갱신 방지**: DB 트랜잭션으로 처리 필수
|
||||
|
||||
### 9.2 성공 지표 (✅ 달성)
|
||||
- [x] Gmail 기존 기능 100% 유지
|
||||
- [x] NAVER WORKS 메일 조회 성공률 100%
|
||||
- [x] 응답 시간 < 2초
|
||||
- [x] 토큰 갱신 후 즉시 사용 가능
|
||||
|
||||
---
|
||||
|
||||
## 10. 참고 문서
|
||||
|
||||
- `/home/admin/DOCS/troubleshooting/250918_naverworks_mail_api_mailaddress_null_issue.md`
|
||||
- `/home/admin/DOCS/troubleshooting/250917_네이버웍스_passport_작업.md`
|
||||
- `/home/admin/DOCS/troubleshooting/250918_gmail_token_slack_user_id_column_missing.md`
|
||||
- `/home/admin/DOCS/300_architecture/sequences/email_sequences.md`
|
||||
|
||||
---
|
||||
|
||||
## 11. 구현 요약 (2025-09-19)
|
||||
|
||||
### 완료 사항
|
||||
1. **Provider 패턴 구현**: Gmail/NAVER WORKS 멀티 프로바이더 지원
|
||||
2. **DB 스키마 수정**: slack_user_id 직접 참조를 user JOIN으로 변경
|
||||
3. **토큰 저장 통일**: token_data JSONB 사용
|
||||
4. **토큰 갱신 구현**: DB 트랜잭션으로 안전한 rotation 처리
|
||||
5. **테스트 성공**: 토큰 갱신 후 메일 30개 조회 확인
|
||||
|
||||
### 서버 설정 필요
|
||||
```bash
|
||||
# skill-email/.env 파일에 추가
|
||||
GOOGLE_CLIENT_ID=(Google OAuth 클라이언트 ID)
|
||||
GOOGLE_CLIENT_SECRET=(Google OAuth 클라이언트 시크릿)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**문서 끝**
|
||||
@ -1,318 +0,0 @@
|
||||
# skill-calendar 멀티플랫폼 캘린더 통합 스킬
|
||||
|
||||
**작성일**: 2025-11-14
|
||||
**작성자**: Claude
|
||||
**목적**: Google/Slack/네이버웍스 캘린더 통합 일정 관리
|
||||
|
||||
---
|
||||
|
||||
## 1. 비즈니스 시나리오
|
||||
|
||||
### 사용자 대화 플로우
|
||||
```
|
||||
사용자: "11월 24일 검진
|
||||
인천 연수구 갯벌로156
|
||||
한국생산기술연구원 200명(의사2인)
|
||||
07:40~약12시"
|
||||
|
||||
로빙: "11월 24일 07:40~12시 검진 일정을 구글 캘린더에 등록해드릴까요?"
|
||||
|
||||
사용자: "그래"
|
||||
|
||||
로빙: "구글 캘린더에 등록 완료했습니다.
|
||||
제목: 한국생산기술연구원 검진
|
||||
일시: 2025-11-24 07:40~12:00
|
||||
장소: 인천 연수구 갯벌로156"
|
||||
```
|
||||
|
||||
### 요구사항
|
||||
1. 사용자(김종태, UUID: 53529291-5050-4daa-89fb-008b546feb63) 식별
|
||||
2. 51123 서버 DB에서 gmail_token 테이블 조회 → Google OAuth 토큰 획득
|
||||
3. Google Calendar API로 일정 등록
|
||||
4. rb8001 ChromaDB에 메모리 저장 (이후 "11월 24일 뭐하지?" 질문에 답변 가능)
|
||||
|
||||
---
|
||||
|
||||
## 2. 아키텍처
|
||||
|
||||
### 서비스 구조
|
||||
```
|
||||
rb8001 (51124)
|
||||
↓ POST /api/events
|
||||
skill-calendar (51124, 포트 8512)
|
||||
↓ SELECT * FROM gmail_token WHERE user_id=?
|
||||
PostgreSQL main_db (51123)
|
||||
↓ Google Calendar API
|
||||
Google Calendar (외부)
|
||||
```
|
||||
|
||||
### 멀티 프로바이더 지원
|
||||
- GoogleCalendarService: gmail_token 테이블 사용
|
||||
- SlackCalendarService: slack_token 테이블 사용 (Slack 타임라인 기능)
|
||||
- NaverWorksCalendarService: naverworks_token 테이블 사용
|
||||
|
||||
---
|
||||
|
||||
## 3. DB 스키마 (51123 서버 main_db)
|
||||
|
||||
### 기존 테이블 활용
|
||||
- gmail_token: Google OAuth 토큰 (Calendar API scope 포함)
|
||||
- slack_token: Slack OAuth 토큰
|
||||
- naverworks_token: 네이버웍스 OAuth 토큰
|
||||
|
||||
### 신규 테이블 (선택)
|
||||
```sql
|
||||
-- 캘린더 이벤트 메타데이터 (선택사항, 동기화 추적용)
|
||||
CREATE TABLE IF NOT EXISTS calendar_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES "user"(id),
|
||||
provider VARCHAR(50) NOT NULL, -- 'google', 'slack', 'naverworks'
|
||||
external_event_id VARCHAR(255) NOT NULL, -- 외부 API의 event_id
|
||||
title TEXT,
|
||||
start_time TIMESTAMP NOT NULL,
|
||||
end_time TIMESTAMP NOT NULL,
|
||||
location TEXT,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, provider, external_event_id)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. skill-calendar API 설계
|
||||
|
||||
### 포트 및 환경변수
|
||||
- PORT: 8512
|
||||
- DATABASE_URL: postgresql://robeings:robeings@192.168.219.45:5432/main_db
|
||||
|
||||
### API 엔드포인트
|
||||
|
||||
#### POST /api/events
|
||||
일정 등록
|
||||
|
||||
**요청**:
|
||||
```json
|
||||
{
|
||||
"user_id": "53529291-5050-4daa-89fb-008b546feb63",
|
||||
"provider": "google", // "google", "slack", "naverworks"
|
||||
"title": "한국생산기술연구원 검진",
|
||||
"start": "2025-11-24T07:40:00",
|
||||
"end": "2025-11-24T12:00:00",
|
||||
"location": "인천 연수구 갯벌로156",
|
||||
"attendees": ["lee.jh@example.com"] // 선택
|
||||
}
|
||||
```
|
||||
|
||||
**응답**:
|
||||
```json
|
||||
{
|
||||
"event_id": "abc123xyz",
|
||||
"provider": "google",
|
||||
"calendar_url": "https://calendar.google.com/calendar/event?eid=..."
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/events
|
||||
일정 조회 (날짜 범위)
|
||||
|
||||
**쿼리**: `?user_id=UUID&provider=google&start_date=2025-11-24&end_date=2025-11-24`
|
||||
|
||||
#### DELETE /api/events/{event_id}
|
||||
일정 삭제
|
||||
|
||||
---
|
||||
|
||||
## 5. rb8001 통합
|
||||
|
||||
### 의도 감지
|
||||
- app/brain/decision_engine.py: "calendar_event" 의도 추가
|
||||
- 키워드: "일정", "캘린더", "등록", 날짜 패턴 (YYYY-MM-DD, MM월 DD일)
|
||||
|
||||
### 스킬 호출
|
||||
- app/skills/calendar_skill.py (신규 생성)
|
||||
- rb8001 → skill-calendar API 호출
|
||||
- 환경변수: SKILL_CALENDAR_URL=http://localhost:8512
|
||||
|
||||
### 메모리 저장
|
||||
- ChromaDB: "11월 24일 검진 일정 등록됨" 저장
|
||||
- Neo4j: event 노드 생성 (선택)
|
||||
|
||||
---
|
||||
|
||||
## 6. TDD 테스트 계획
|
||||
|
||||
### 테스트 파일
|
||||
- skill-calendar/tests/test_google_calendar_integration.py
|
||||
- rb8001/tests/test_calendar_intent_detection.py
|
||||
|
||||
### 시나리오 1: 구글 캘린더 일정 등록
|
||||
```python
|
||||
Given: 김종태(user_id)의 gmail_token 존재
|
||||
When: POST /api/events (제목, 날짜, 시간, 장소)
|
||||
Then:
|
||||
- Google Calendar API 호출 성공
|
||||
- event_id 반환
|
||||
- calendar_events 테이블 저장 (선택)
|
||||
```
|
||||
|
||||
### 시나리오 2: rb8001 의도 감지 및 2단계 대화
|
||||
```python
|
||||
Given: "11월 24일 검진..." 메시지 입력
|
||||
When: rb8001 의도 분석
|
||||
Then:
|
||||
- intent="calendar_event" 감지
|
||||
- entities={날짜, 시간, 장소} 추출
|
||||
- "일정 등록해드릴까요?" 응답
|
||||
- 다음 메시지 "그래" 입력 시 skill-calendar 호출
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 구현 순서
|
||||
|
||||
### Phase 1: skill-calendar 기본 뼈대 (2시간)
|
||||
1. skill-calendar 디렉토리 생성 (Gitea 레포 연동)
|
||||
2. FastAPI 앱 구조 생성
|
||||
3. GoogleCalendarService 클래스 구현
|
||||
4. POST /api/events 엔드포인트 구현
|
||||
5. DB 연결 (51123 main_db)
|
||||
|
||||
### Phase 2: Google Calendar API 통합 (1시간)
|
||||
1. gmail_token 테이블 조회 로직
|
||||
2. google-api-python-client로 Calendar API 호출
|
||||
3. 토큰 갱신 로직 (skill_email 패턴 참조)
|
||||
4. 에러 처리 (401, 403, 429)
|
||||
|
||||
### Phase 3: rb8001 통합 (1시간)
|
||||
1. app/skills/calendar_skill.py 생성
|
||||
2. app/brain/decision_engine.py에 calendar_event 의도 추가
|
||||
3. 2단계 대화 플로우 (확인 → 실행)
|
||||
4. ChromaDB 메모리 저장
|
||||
|
||||
### Phase 4: TDD 테스트 (1시간)
|
||||
1. test_google_calendar_integration.py 작성
|
||||
2. Mock 테스트 (Calendar API)
|
||||
3. E2E 테스트 (실제 토큰 사용)
|
||||
|
||||
### Phase 5: 배포 및 검증 (30분)
|
||||
1. docker-compose.yml 작성
|
||||
2. Gitea Actions 설정
|
||||
3. 실제 시나리오 테스트 (김종태 계정)
|
||||
|
||||
---
|
||||
|
||||
## 8. 참고 문서
|
||||
|
||||
- DOCS/troubleshooting/250917_네이버웍스_캘린더_API_연동_가이드.md
|
||||
- DOCS/troubleshooting/250820_happybell80_Gmail패스포트시스템완성.md
|
||||
- skill_email/services/gmail_service.py:38-83 (Gmail API 패턴)
|
||||
- rb8001/app/services/naverworks_file_processor.py (스킬 호출 패턴)
|
||||
|
||||
---
|
||||
|
||||
## 9. 제약사항
|
||||
|
||||
### Google Calendar API
|
||||
- Scope: calendar.events (gmail_token에 이미 포함 여부 확인 필요)
|
||||
- Quota: 무료 tier 하루 1백만 요청 (충분)
|
||||
- Auth: OAuth 2.0 (skill_email과 동일 토큰 공유)
|
||||
|
||||
### 보안
|
||||
- 51123 DB 접근: read-only 권장 (gmail_token 조회만)
|
||||
- 토큰 갱신: skill_email 패턴 따름
|
||||
- CORS: rb8001에서만 호출 (내부 네트워크)
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 10. 구현 현황
|
||||
|
||||
### Phase 1: 기본 뼈대 구현 완료 (2025-11-14)
|
||||
|
||||
**Git 레포**: https://git.ro-being.com/ivada_Ro-being/skill-calendar.git
|
||||
**커밋**: 15bcd9c
|
||||
**구현 파일**:
|
||||
- services/google_calendar_service.py: Gmail 토큰 조회 + Calendar API
|
||||
- routers/calendar.py: POST/GET/DELETE /api/events
|
||||
- tests/test_google_calendar_integration.py: TDD 테스트 (Red Phase)
|
||||
- main.py: FastAPI 앱 (포트 8512)
|
||||
- docker-compose.yml: 51123 DB 연결
|
||||
|
||||
**TDD 상태**:
|
||||
- ✅ Import 성공
|
||||
- ✅ gmail_token 조회 성공
|
||||
- ❌ Calendar API 호출: invalid_grant (토큰 만료 또는 calendar scope 부족)
|
||||
|
||||
### Phase 2: Docker 배포 및 rb8001 통합 완료 (2025-11-14)
|
||||
|
||||
**skill-calendar**:
|
||||
- ✅ Docker 빌드 성공
|
||||
- ✅ 컨테이너 시작 (포트 8512, healthy)
|
||||
- ✅ Gitea Actions 자동 배포 설정
|
||||
|
||||
**rb8001 통합**:
|
||||
- ✅ app/skills/calendar_skill.py 생성
|
||||
- ✅ SKILL_CALENDAR_URL=http://localhost:8512 추가
|
||||
- ✅ Git 푸시 완료 (커밋 7c00027)
|
||||
|
||||
### Phase 3: rb8001 의도 감지 완료 (2025-11-14)
|
||||
|
||||
**decision_engine 통합**:
|
||||
- ✅ IntentType.CALENDAR_EVENT 추가
|
||||
- ✅ 날짜+시간 패턴 매칭 (11월 24일 07:40 등)
|
||||
- ✅ skill_sequences에 calendar_confirm 액션 추가
|
||||
- ✅ 테스트: "11월 24일 검진..." → calendar_event (신뢰도 0.9)
|
||||
- ✅ Git 푸시 완료 (커밋 477fd1c)
|
||||
- ✅ rb8001 재배포 완료
|
||||
|
||||
**현재 동작**:
|
||||
1. 사용자: "11월 24일 검진..." 입력
|
||||
2. decision_engine: calendar_event 의도 감지 (0.9)
|
||||
3. LLM: 일정 정보 파싱 및 확인 메시지 생성
|
||||
|
||||
### Phase 4: calendar_confirm LLM 처리 완료 (2025-11-14)
|
||||
|
||||
**LLM 통합**:
|
||||
- ✅ LLMRequest task_type에 'calendar_confirm' 추가
|
||||
- ✅ llm_service.py에 calendar_confirm 처리 로직 구현
|
||||
- ✅ 시스템 프롬프트: 날짜/시간/장소 파싱 및 확인 메시지 생성
|
||||
- ✅ Git 푸시 완료 (커밋 8fb94d9)
|
||||
- ✅ rb8001 재배포 완료
|
||||
|
||||
**현재 동작**:
|
||||
1. 사용자: "11월 24일 검진 07:40~12시..." 입력
|
||||
2. decision_engine: calendar_event 의도 감지 (0.9)
|
||||
3. LLM (calendar_confirm): 일정 정보 파싱 → "일정을 구글 캘린더에 등록해드릴까요?" 응답
|
||||
|
||||
### Phase 5: approval 로직 및 CalendarSkill 호출 완료 (2025-11-14)
|
||||
|
||||
**컨텍스트 기반 approval**:
|
||||
- ✅ analyze_intent()에 context 매개변수 추가
|
||||
- ✅ 긍정 응답("그래/ㅇㅇ/네/예") 감지 로직
|
||||
- ✅ _confirm/_request 접미사만 _approval로 전환
|
||||
- ✅ 10분 시간 제약 적용
|
||||
- ✅ TDD 테스트 10/10 통과
|
||||
|
||||
**calendar_handler.py**:
|
||||
- ✅ LLM 응답에서 일정 정보 파싱 (날짜, 시간, 장소, 제목)
|
||||
- ✅ CalendarSkill.create_event() 호출
|
||||
- ✅ 성공 메시지 생성
|
||||
- ✅ Git 푸시 완료 (커밋 b4495c3)
|
||||
- ✅ rb8001 재배포 완료
|
||||
|
||||
**전체 플로우 완성**:
|
||||
1. 사용자: "11월 24일 검진 07:40~12시..." 입력
|
||||
2. calendar_event 감지 (0.9) → calendar_confirm
|
||||
3. 로빙: "일정을 구글 캘린더에 등록해드릴까요?"
|
||||
4. 사용자: "그래"
|
||||
5. calendar_approval 감지 (0.95) → create_event
|
||||
6. CalendarSkill → skill-calendar API → Google Calendar
|
||||
7. 로빙: "✅ 구글 캘린더에 일정을 등록했습니다!"
|
||||
|
||||
**남은 작업**:
|
||||
1. Gmail Passport calendar scope 재연동 (invalid_grant 해결)
|
||||
2. E2E 실제 테스트
|
||||
|
||||
|
||||
@ -1,389 +0,0 @@
|
||||
# 의도 파악 3단계 아키텍처 전환 계획
|
||||
|
||||
**작성일**: 2025-11-26
|
||||
**작성자**: Auto (Claude)
|
||||
**상태**: 구현 완료 (2025-11-26)
|
||||
**관련 문서**:
|
||||
- `251017_intent_analysis_improvement_plan.md` - 의도 분석 개선 플랜
|
||||
- `251023_happybell80_의도_런타임_하이브리드_임베딩_베이지안_동적학습.md` - 의도 런타임 설계
|
||||
- `311_FastAPI_구조_원칙.md` - 코드 작성 원칙
|
||||
|
||||
---
|
||||
|
||||
## 1. 문제 발견
|
||||
|
||||
### 1.1 현재 구조의 근본적 문제
|
||||
|
||||
**발견된 문제**:
|
||||
- IntentType이 스킬/액션과 직접 1:1 매핑되어 있음
|
||||
- 의도 파악이 곧 스킬 선택으로 이어지는 구조
|
||||
- 진정한 "의도 파악(무엇을 원하는가)" → "행동 계획(어떻게 할 것인가)" → "스킬 선택(어떤 도구를 쓸 것인가)"의 단계적 분리가 없음
|
||||
|
||||
**실제 문제 사례**:
|
||||
```
|
||||
사용자: "12월 10일 11시 종합운동장에서 뵙겠습니다. 준비물: 노트북,휴대폰 일정등록"
|
||||
로빙: "네, 종합운동장 미팅 일정을 12월 10일 11시에 등록해 드릴게요. 구글 캘린더에 등록해드릴까요?"
|
||||
사용자: "그래"
|
||||
로빙: "안녕하세요! 무엇을 도와드릴까요? 😊" # ❌ 일반 인사로 돌아감
|
||||
```
|
||||
|
||||
**원인 분석**:
|
||||
- "구글 캘린더에 등록해드릴까요?" 응답이 `calendar_event` intent로 저장됨
|
||||
- "그래" 같은 동의 메시지가 `_confirm`으로 끝나는 intent를 찾지 못함
|
||||
- `calendar_event`는 approval 대상이 아니어서 `calendar_approval`로 변환되지 않음
|
||||
|
||||
### 1.2 아키텍처 구조적 문제
|
||||
|
||||
**현재 구조**:
|
||||
```
|
||||
IntentType (스킬/액션과 직접 매핑)
|
||||
↓
|
||||
decide_skill_sequence() → 스킬 선택
|
||||
```
|
||||
|
||||
**문제점**:
|
||||
- 의도 파악이 스킬 선택에 가까움
|
||||
- 새로운 의도 추가 시 스킬도 함께 정의해야 함
|
||||
- 의도와 구현이 강하게 결합됨
|
||||
|
||||
---
|
||||
|
||||
## 2. 개선 방향
|
||||
|
||||
### 2.1 3단계 구조 설계
|
||||
|
||||
**목표**: 의도 파악 → 행동 계획 → 스킬 선택의 단계적 접근
|
||||
|
||||
```
|
||||
1단계: 의도 파악 (IntentAnalyzer)
|
||||
- 추상적 목표 파악 (일정 관리, 정보 검색, 커뮤니케이션 등)
|
||||
- LLM 기반 제로샷 의도 분석 (무한한 의도 처리 가능)
|
||||
- 사용자가 무엇을 원하는지 추상적 수준에서 이해
|
||||
|
||||
2단계: 행동 계획 (ActionPlanner)
|
||||
- 구체적 행동 결정 (등록/조회/삭제/수정 등)
|
||||
- 의도를 달성하기 위한 구체적 행동 계획
|
||||
- 여러 행동이 필요한 경우 순서 및 우선순위 결정
|
||||
|
||||
3단계: 스킬 선택 (SkillSelector)
|
||||
- 적절한 도구 선택 (calendar_skill.create_event 등)
|
||||
- 행동을 수행할 적절한 스킬 선택
|
||||
- 스킬 실행 가능 여부 확인
|
||||
```
|
||||
|
||||
### 2.2 3단계 구조의 장점
|
||||
|
||||
- **의도와 구현의 분리**: 같은 의도라도 다른 행동/스킬로 구현 가능
|
||||
- **확장성**: 새로운 스킬 추가 시 기존 의도 재사용 가능
|
||||
- **유연성**: 행동 계획 단계에서 여러 대안 고려 가능
|
||||
- **명확성**: 각 단계의 책임이 명확히 분리됨
|
||||
- **무한한 의도 처리**: LLM 기반으로 아직 구현되지 않은 의도도 파악 가능
|
||||
|
||||
---
|
||||
|
||||
## 3. 구현 계획
|
||||
|
||||
### 3.1 폴더 구조
|
||||
|
||||
```
|
||||
app/services/brain/
|
||||
├── intent/ # 신규 폴더
|
||||
│ ├── __init__.py
|
||||
│ ├── schemas.py # IntentGoal, ActionPlan, SkillSequence
|
||||
│ ├── intent_analyzer.py # 1단계: 의도 파악
|
||||
│ ├── action_planner.py # 2단계: 행동 계획
|
||||
│ ├── skill_selector.py # 3단계: 스킬 선택
|
||||
│ ├── intent_graph.py # 기존 파일 이동
|
||||
│ └── semantic_classifier.py # 기존 파일 이동
|
||||
├── decision_engine.py # 래퍼로 유지 (하위 호환성)
|
||||
├── brain_service.py # 3단계 파이프라인 호출
|
||||
└── ...
|
||||
```
|
||||
|
||||
### 3.2 데이터 구조 정의
|
||||
|
||||
**Pydantic 모델** (`app/services/brain/intent/schemas.py`):
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List, Dict, Any
|
||||
from enum import Enum
|
||||
|
||||
class IntentCategory(str, Enum):
|
||||
"""추상적 의도 카테고리"""
|
||||
SCHEDULE_MANAGEMENT = "schedule_management" # 일정 관리
|
||||
INFORMATION_RETRIEVAL = "information_retrieval" # 정보 검색
|
||||
COMMUNICATION = "communication" # 커뮤니케이션
|
||||
DOCUMENT_PROCESSING = "document_processing" # 문서 처리
|
||||
TASK_EXECUTION = "task_execution" # 작업 실행
|
||||
GENERAL_INQUIRY = "general_inquiry" # 일반 질의
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
class IntentGoal(BaseModel):
|
||||
"""1단계: 의도 파악 결과"""
|
||||
category: IntentCategory # 추상적 카테고리
|
||||
description: str # LLM이 파악한 의도 설명
|
||||
confidence: float # 신뢰도 (0.0 ~ 1.0)
|
||||
raw_intent: Optional[str] = None # LLM 원본 출력 (디버깅용)
|
||||
context_hints: Dict[str, Any] = {} # 컨텍스트 힌트
|
||||
|
||||
class ActionType(str, Enum):
|
||||
"""구체적 행동 타입"""
|
||||
CREATE_EVENT = "create_event"
|
||||
QUERY_EVENTS = "query_events"
|
||||
DELETE_EVENT = "delete_event"
|
||||
UPDATE_EVENT = "update_event"
|
||||
SEARCH_WEB = "search_web"
|
||||
SEARCH_MEMORY = "search_memory"
|
||||
SEND_EMAIL = "send_email"
|
||||
READ_EMAIL = "read_email"
|
||||
ANALYZE_DOCUMENT = "analyze_document"
|
||||
CLARIFY = "clarify"
|
||||
CHAT = "chat"
|
||||
|
||||
class ActionPlan(BaseModel):
|
||||
"""2단계: 행동 계획 결과"""
|
||||
actions: List[Dict[str, Any]] # 행동 목록
|
||||
requires_clarification: bool = False
|
||||
clarification_message: Optional[str] = None
|
||||
|
||||
class SkillSequence(BaseModel):
|
||||
"""3단계: 스킬 선택 결과 (기존 execution_plan과 호환)"""
|
||||
skills: List[Dict[str, Any]] # 스킬 목록
|
||||
intent: Dict[str, Any] # 원본 의도 정보 (하위 호환성)
|
||||
```
|
||||
|
||||
### 3.3 데이터 흐름
|
||||
|
||||
```
|
||||
IntentAnalyzer.analyze() → IntentGoal
|
||||
↓
|
||||
ActionPlanner.plan(IntentGoal) → ActionPlan
|
||||
↓
|
||||
SkillSelector.select(ActionPlan) → SkillSequence
|
||||
↓
|
||||
기존 execution_plan 형식으로 변환 (하위 호환성)
|
||||
```
|
||||
|
||||
### 3.4 하위 호환성 유지
|
||||
|
||||
**기존 DecisionEngine 클래스는 래퍼로 유지**:
|
||||
- 내부적으로 3단계 파이프라인 호출
|
||||
- 기존 API 인터페이스 유지
|
||||
- 점진적 마이그레이션 가능
|
||||
|
||||
---
|
||||
|
||||
## 4. TDD 시나리오
|
||||
|
||||
### 시나리오 1: 일정 등록 동의 처리 (현재 문제 해결)
|
||||
```
|
||||
사용자: "12월 10일 11시 종합운동장에서 뵙겠습니다. 준비물: 노트북,휴대폰 일정등록"
|
||||
로빙: "네, 종합운동장 미팅 일정을 12월 10일 11시에 등록해 드릴게요. 구글 캘린더에 등록해드릴까요?"
|
||||
사용자: "그래"
|
||||
→ IntentGoal: {category: SCHEDULE_MANAGEMENT, description: "일정 등록 승인"}
|
||||
→ ActionPlan: {actions: [{type: CREATE_EVENT, goal: "구글 캘린더에 일정 등록"}]}
|
||||
→ SkillSequence: {skills: [{skill: "CALENDAR", action: "create_event"}]}
|
||||
→ 성공: 일정 등록 완료
|
||||
```
|
||||
|
||||
### 시나리오 2: 아직 구현되지 않은 의도 처리
|
||||
```
|
||||
사용자: "내일 오후 2시 회의실 예약해줘"
|
||||
→ IntentGoal: {category: SCHEDULE_MANAGEMENT, description: "회의실 예약 요청"}
|
||||
→ ActionPlan: {actions: [{type: CREATE_EVENT, goal: "회의실 예약"}]}
|
||||
→ SkillSelector: 회의실 예약 스킬 없음 감지
|
||||
→ 로빙: "죄송합니다. 회의실 예약 기능은 아직 준비 중입니다. 구글 캘린더에 일정만 등록해드릴까요?"
|
||||
```
|
||||
|
||||
### 시나리오 3: 복합 의도 처리
|
||||
```
|
||||
사용자: "리버스마운틴 유사 기업 찾아서 가치평가 보고서 이메일로 보내줘"
|
||||
→ IntentGoal: {category: INFORMATION_RETRIEVAL, description: "기업 검색 및 분석 후 이메일 발송"}
|
||||
→ ActionPlan: {actions: [
|
||||
{type: SEARCH_WEB, goal: "리버스마운틴 유사 기업 검색", priority: 1},
|
||||
{type: ANALYZE_DOCUMENT, goal: "가치평가 분석", priority: 2},
|
||||
{type: SEND_EMAIL, goal: "보고서 이메일 발송", priority: 3}
|
||||
]}
|
||||
→ SkillSequence: {skills: [
|
||||
{skill: "TOOL", action: "web_search"},
|
||||
{skill: "LLM", action: "analyze"},
|
||||
{skill: "EMAIL", action: "compose"}
|
||||
]}
|
||||
```
|
||||
|
||||
### 시나리오 4: 맥락 기반 의도 파악
|
||||
```
|
||||
사용자: "아까 말한 그 기업 투자 단계는?"
|
||||
→ IntentGoal: {category: INFORMATION_RETRIEVAL, description: "이전 대화에서 언급된 기업의 투자 단계 조회", context_hints: {reference: "previous_conversation"}}
|
||||
→ ActionPlan: {actions: [{type: SEARCH_MEMORY, goal: "이전 대화에서 기업 정보 검색"}]}
|
||||
→ SkillSequence: {skills: [{skill: "LLM", action: "search_memory"}]}
|
||||
→ 성공: ChromaDB에서 관련 대화 검색 후 답변
|
||||
```
|
||||
|
||||
### 시나리오 5: 모호한 의도 명확화
|
||||
```
|
||||
사용자: "이메일 보내줘"
|
||||
→ IntentGoal: {category: COMMUNICATION, description: "이메일 발송 요청", confidence: 0.6}
|
||||
→ ActionPlan: {
|
||||
requires_clarification: True,
|
||||
clarification_message: "누구에게 보내실까요? 어떤 내용을 보내실까요?"
|
||||
}
|
||||
→ SkillSequence: {skills: []}
|
||||
→ 로빙: "누구에게 보내실까요? 어떤 내용을 보내실까요?"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 수정 범위
|
||||
|
||||
### 5.1 수정 필요한 부분
|
||||
|
||||
**백엔드 (rb8001)**:
|
||||
- `app/services/brain/decision_engine.py` → 3단계 파이프라인으로 재구성
|
||||
- `app/services/brain/intent/` 폴더 신규 생성
|
||||
- `app/services/brain/brain_service.py` → 3단계 파이프라인 호출
|
||||
|
||||
### 5.2 수정 불필요한 부분
|
||||
|
||||
- **Gateway (robeing-gateway)**: 변경 불필요, JWT 검증/프록시 역할 유지
|
||||
- **프론트엔드**: 변경 불필요, API 인터페이스 동일
|
||||
- **DB 스키마**: 변경 불필요, 의도/행동 계획은 런타임 처리
|
||||
|
||||
---
|
||||
|
||||
## 6. 구현 순서
|
||||
|
||||
### Phase 1: 구조 설계 및 스키마 정의 (1일)
|
||||
1. `app/services/brain/intent/` 폴더 생성
|
||||
2. `schemas.py` 작성 (IntentGoal, ActionPlan, SkillSequence)
|
||||
3. 기존 파일 이동 (intent_graph.py, semantic_classifier.py)
|
||||
|
||||
### Phase 2: IntentAnalyzer 구현 (2일)
|
||||
1. LLM 기반 제로샷 의도 분석
|
||||
2. IntentCategory 분류
|
||||
3. TDD 테스트 작성
|
||||
|
||||
### Phase 3: ActionPlanner 구현 (2일)
|
||||
1. IntentGoal → ActionPlan 변환 로직
|
||||
2. 복합 행동 처리
|
||||
3. Clarify 로직
|
||||
|
||||
### Phase 4: SkillSelector 구현 (2일)
|
||||
1. ActionPlan → SkillSequence 변환
|
||||
2. 기존 execution_plan 형식으로 변환
|
||||
3. 하위 호환성 유지
|
||||
|
||||
### Phase 5: 통합 및 테스트 (2일)
|
||||
1. DecisionEngine 래퍼 구현
|
||||
2. 기존 테스트 통과 확인
|
||||
3. TDD 시나리오 검증
|
||||
|
||||
---
|
||||
|
||||
## 7. 참고 사항
|
||||
|
||||
### 7.1 코드 작성 원칙 준수
|
||||
- 계층 분리: router → services → state
|
||||
- 파일 크기: 한 파일 최대 300줄
|
||||
- 의존성 방향: 단방향 흐름
|
||||
|
||||
### 7.2 하위 호환성
|
||||
- 기존 DecisionEngine API 유지
|
||||
- 기존 execution_plan 형식 유지
|
||||
- 점진적 마이그레이션 가능
|
||||
|
||||
---
|
||||
|
||||
**작성 완료**: 2025-11-26
|
||||
**다음 단계**: Phase 1 구현 시작
|
||||
|
||||
---
|
||||
|
||||
## 8. 구현 완료 (2025-11-26)
|
||||
|
||||
### 8.1 구현 내용
|
||||
|
||||
**완료된 작업**:
|
||||
1. ✅ IntentAnalyzer 구현 (`app/services/brain/intent/intent_analyzer.py`)
|
||||
- LLM 기반 제로샷 의도 분석 (async)
|
||||
- 키워드 기반 폴백 분석 (sync)
|
||||
- IntentCategory 분류 지원
|
||||
|
||||
2. ✅ ActionPlanner 확장 (`app/services/brain/intent/action_planner.py`)
|
||||
- 모든 IntentCategory에 대한 ActionType 매핑
|
||||
- SCHEDULE_MANAGEMENT: CREATE_EVENT, QUERY_EVENTS, DELETE_EVENT, UPDATE_EVENT
|
||||
- INFORMATION_RETRIEVAL: SEARCH_WEB, SEARCH_MEMORY
|
||||
- COMMUNICATION: SEND_EMAIL, READ_EMAIL
|
||||
- DOCUMENT_PROCESSING: ANALYZE_DOCUMENT
|
||||
- TASK_EXECUTION, GENERAL_INQUIRY: CHAT
|
||||
- UNKNOWN: CLARIFY
|
||||
|
||||
3. ✅ SkillSelector 확장 (`app/services/brain/intent/skill_selector.py`)
|
||||
- 모든 ActionType에 대한 스킬 매핑
|
||||
- CALENDAR, EMAIL, TOOL, LLM 스킬 지원
|
||||
|
||||
4. ✅ DecisionEngine 통합 (`app/services/brain/decision_engine.py`)
|
||||
- `_build_intent_pipeline_sync`: 동기 버전 (폴백 분석)
|
||||
- `_build_intent_pipeline`: 비동기 버전 (LLM 기반)
|
||||
- `create_execution_plan`에 `intent_pipeline` 메타데이터 추가
|
||||
|
||||
5. ✅ 테스트 통과
|
||||
- ActionPlanner 테스트 통과
|
||||
- SkillSelector 테스트 통과
|
||||
- DecisionEngine 통합 테스트 통과
|
||||
|
||||
### 8.2 배포 상태
|
||||
|
||||
- **커밋**: `1b57c86` - "feat: 3단계 의도 파악 아키텍처 구현"
|
||||
- **배포 완료**: 2025-11-26 19:17
|
||||
- **컨테이너 상태**: 정상 (healthy)
|
||||
|
||||
### 8.3 하위 호환성
|
||||
|
||||
- 기존 `decide_skill_sequence` 메서드 유지 (하위 호환성)
|
||||
- 새로운 3단계 파이프라인은 `execution_plan["intent_pipeline"]` 메타데이터로 제공
|
||||
- 점진적 마이그레이션을 위해 두 시스템 병행
|
||||
|
||||
### 8.4 중복 코드 확인
|
||||
|
||||
- `decide_skill_sequence`와 `SkillSelector` 모두 IntentType/ActionType → Skill 매핑 포함
|
||||
- 의도적 중복: 하위 호환성 및 점진적 마이그레이션을 위해 병행 유지
|
||||
- 주석 추가로 의도 명확화
|
||||
|
||||
---
|
||||
|
||||
## 9. DB/베이지안 방법 통합 방안
|
||||
|
||||
### 9.1 문제
|
||||
|
||||
5가지 TDD 시나리오 테스트 결과 시나리오 2~5가 미비:
|
||||
- 시나리오 2: 미구현 의도 처리 - clarify 없음
|
||||
- 시나리오 3: 복합 의도 처리 - 단일 액션만 생성
|
||||
- 시나리오 4: 맥락 기반 의도 파악 - UNKNOWN 분류
|
||||
- 시나리오 5: 모호한 의도 명확화 - clarify 없음
|
||||
|
||||
### 9.2 해결 방안
|
||||
|
||||
기존 DB/베이지안 방법(`intent_prototypes`, `intent_thresholds`, `intent_path_stats`)을 3단계 아키텍처에 통합:
|
||||
- **IntentAnalyzer**: `SemanticIntentClassifier` 재사용, 임베딩 기반 후보 축소
|
||||
- **ActionPlanner**: `intent_path_stats`의 Beta(α,β)로 액션 우선순위 조정, 복합 의도 파싱
|
||||
- **SkillSelector**: 스킬별 실행 성공률 추적, 미구현 스킬 감지
|
||||
|
||||
**상세 내용**: `251126_intent_3step_db_bayesian_integration.md` 참고
|
||||
|
||||
---
|
||||
|
||||
## 10. 리스크 및 추가 고려사항
|
||||
|
||||
- **회귀 및 승인/컨펌 흐름 붕괴 리스크**
|
||||
- 기존 DecisionEngine이 암묵적으로 처리하던 승인/취소/컨펌, 예외 케이스들이 IntentGoal → ActionPlan → SkillSequence 분리 과정에서 깨질 수 있음 (예: 같은 입력인데 캘린더/이메일 오작동).
|
||||
- 실제 서비스 대화 로그를 기반으로 한 리플레이 테스트 세트, A/B 플래그(구 구조/신 구조 토글), 빠른 롤백 전략을 설계한 뒤 단계적으로 트래픽을 전환해야 함.
|
||||
|
||||
- **LLM 기반 1단계 도입에 따른 비용/레이턴시/안정성 이슈**
|
||||
- 모든 발화가 최소 1회 LLM을 타면 레이턴시·비용·비결정성이 커지므로, 고빈도·단순 패턴(“그래”, “응”, 일정 단문 등)은 규칙/통계 모델로 단축 경로를 두고, 각 단계별 confidence 기준과 실패 시 폴백 전략(UNKNOWN → 안전한 일반 대화/질문 재확인)을 명시적으로 정의해야 함.
|
||||
- LLM 프롬프트/파라미터 변경에 따른 드리프트를 감시할 수 있도록, IntentAnalyzer 입력/출력(카테고리, 설명, confidence)을 구조화된 로그로 남기고 모니터링 지표(오탐/미탐 비율, 평균 응답 시간 등)를 운영 레벨에서 관리해야 함.
|
||||
|
||||
- **학습·개선 루프까지 포함한 설계**
|
||||
- 3단계 분리는 파일 구조 정리가 아니라, 각 단계에서 생성되는 신호를 intent_review/active_learning과 연결해 시간이 지날수록 의도→행동→스킬 품질이 자동 개선되도록 만드는 것이 핵심.
|
||||
- IntentGoal/ActionPlan/SkillSequence 및 실제 실행 결과(성공/실패, 사용자 피드백)를 표준 스키마로 저장하고, 이를 기반으로 한 오프라인 평가·액티브 러닝·온라인 튜닝 루프를 설계해야 함.
|
||||
Loading…
x
Reference in New Issue
Block a user