diff --git a/email_skill_integration_status.md b/email_skill_integration_status.md new file mode 100644 index 0000000..f1457f1 --- /dev/null +++ b/email_skill_integration_status.md @@ -0,0 +1,294 @@ +# Email Skill 통합 현황 및 TODO + +## 작성일: 2025-08-13 +## 작성자: Claude (with heejae) + +--- + +## 1. 현재 상황 + +### 1.1 시스템 구조 +``` +User → rb8001 → skill-email → Gmail API + ↓ + router.py가 라우팅 + ↓ + skill-email:8501 +``` + +### 1.2 발생한 문제 +**오류**: `422 Unprocessable Entity` + +**원인 1**: Gmail OAuth 토큰 파일 없음 +- 필요 위치: `/home/admin/auth-server/tokens/test_gmail.json` +- 현재 상태: 디렉토리는 있지만 토큰 파일 없음 + +**원인 2**: 잘못된 요청 형식 +```python +# 현재 rb8001이 보내는 payload +{ + "message": "이메일 보내줘", + "user_id": "U091UNVE41M", + "channel": "C123456", + "robing_id": "rb8001", + "action": "compose", + "execution_plan": {...} +} + +# skill-email이 기대하는 payload +{ + "to": "recipient@example.com", + "subject": "제목", + "body": "내용", + "user_id": "test" # Gmail 토큰 식별용 +} +``` + +--- + +## 2. 토큰 관리 방안 + +### 2.1 현재 구조 (파일 기반) +```yaml +위치: /home/admin/auth-server/tokens/ +형식: {user_id}_gmail.json +내용: + - token: "access_token" + - refresh_token: "refresh_token" + - client_id: "xxx.apps.googleusercontent.com" + - client_secret: "secret" + - scopes: ["gmail.send", "gmail.readonly"] +``` + +### 2.2 제안: PostgreSQL 기반 관리 +```sql +-- Gmail 토큰 테이블 +CREATE TABLE gmail_tokens ( + id SERIAL PRIMARY KEY, + user_id VARCHAR(100) UNIQUE NOT NULL, + robing_id VARCHAR(50), + token TEXT NOT NULL, + refresh_token TEXT NOT NULL, + token_uri VARCHAR(255), + client_id VARCHAR(255), + client_secret VARCHAR(255), + scopes TEXT, -- JSON array + expiry TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +**장점**: +- 중앙 집중식 관리 +- 여러 서버에서 접근 가능 +- 백업/복구 용이 +- 토큰 갱신 이력 추적 + +--- + +## 3. 아키텍처 개선 방안 + +### 3.1 현재 문제점 +- rb8001이 이메일 구조를 알아야 함 +- skill-email이 자연어 처리 못함 +- 여러 로빙이 하나의 skill-email 공유 시 충돌 + +### 3.2 개선안: 하이브리드 접근 +```python +# rb8001 - 의도 파악만 +async def route_email(message): + intent = extract_intent(message) # "이메일 보내줘" + hints = extract_entities(message) # {recipient_hint: "희재"} + + return await skill_email.compose({ + "original_message": message, + "hints": hints, + "slack_user_id": user_id, + "robing_id": robing_id + }) + +# skill-email - 이메일 생성 및 발송 +async def compose_email(request): + # 1. 자연어에서 이메일 정보 추출 + email_data = await parse_email_request( + request.original_message, + request.hints + ) + + # 2. 부족한 정보 확인 + if not email_data.complete: + return {"need_info": ["recipient", "subject"]} + + # 3. Gmail 토큰 확인 + token = await get_token(request.slack_user_id) + if not token: + return {"error": "No Gmail token"} + + # 4. 발송 + return await send_via_gmail(email_data, token) +``` + +--- + +## 4. TODO List + +### Phase 1: 긴급 수정 (토큰 없이 테스트) +- [ ] skill-email에 mock 모드 추가 + ```python + @app.post("/compose") + async def compose_mock(request): + # 이메일 구성만, 실제 발송 안 함 + return { + "status": "composed", + "preview": { + "to": "test@example.com", + "subject": f"Re: {request.message[:30]}", + "body": f"About: {request.message}" + }, + "error": "No Gmail token (mock mode)" + } + ``` + +- [ ] rb8001 router 수정 + - 이메일 intent 감지 로직 개선 + - skill-email로 원본 메시지 전달 + +### Phase 2: 토큰 시스템 구축 +- [ ] Gmail OAuth 플로우 구현 + ```python + # auth-server에 추가 + @app.get("/oauth/gmail/authorize") + async def gmail_auth(): + # Google OAuth URL 생성 + return redirect(google_oauth_url) + + @app.get("/oauth/gmail/callback") + async def gmail_callback(code: str): + # 토큰 교환 및 저장 + token = exchange_code_for_token(code) + save_token_to_db(token) + ``` + +- [ ] PostgreSQL 토큰 테이블 생성 +- [ ] robeing-state-service에 토큰 API 추가 + +### Phase 3: 사용자 매핑 +- [ ] Slack User ID → Gmail 계정 매핑 + ```python + USER_GMAIL_MAPPING = { + "U0925SXQFDK": "heejae", # 희재 + "U091UNVE41M": "jongtae", # 종태 + "U092HQMUXMB": "hanyong" # 한용 + } + ``` + +- [ ] 로빙별 발신 계정 설정 + ```python + ROBING_GMAIL_MAPPING = { + "rb8001": "robeing.main", + "rb10408": "robeing.test", + "rb10508": "robeing.micro" + } + ``` + +### Phase 4: LLM 통합 +- [ ] skill-email에 경량 LLM 추가 (선택사항) +- [ ] 이메일 템플릿 시스템 +- [ ] 주소록 검색 기능 + +### Phase 5: 프로덕션 준비 +- [ ] 토큰 자동 갱신 (refresh_token 사용) +- [ ] 에러 처리 및 재시도 로직 +- [ ] 발송 이력 저장 +- [ ] 보안 검토 (토큰 암호화) + +--- + +## 5. 서버 정보 + +### Auth 서버 (PostgreSQL 위치) +```bash +Host: 124.55.18.179 +Port: 51123 +User: heejae +Database: auth_db +PostgreSQL User: robeings/robeings +``` + +### SSH 터널 (현재 설정) +```bash +# 로컬 5433 → 원격 5432 +ssh -L 5433:localhost:5432 admin@124.55.18.179 -p 51123 +``` + +### 관련 서비스 +- **rb8001**: 메인 로빙 (포트 8001) +- **skill-email**: 이메일 스킬 (포트 8501) +- **robeing-state-service**: 상태 관리 (포트 8507) + +--- + +## 6. 결정 필요 사항 + +### Q1: LLM 위치 +- **Option A**: skill-email에 자체 LLM (독립적) +- **Option B**: rb8001에서 처리 후 전달 (중앙 집중) +- **Option C**: 하이브리드 (기본은 rb8001, 복잡한 경우 skill-email) ✅ + +### Q2: 토큰 저장 방식 +- **Option A**: 파일 시스템 (현재) +- **Option B**: PostgreSQL (제안) ✅ +- **Option C**: Redis (임시 캐시) + +### Q3: 멀티 로빙 지원 +- 각 로빙이 다른 Gmail 계정 사용? +- 사용자별로 Gmail 계정 매핑? +- 공용 계정 하나로 통합? + +--- + +## 7. 참고 코드 + +### 현재 skill-email 구조 +```python +# main.py +TOKEN_BASE = os.getenv("TOKEN_BASE", "../auth-server/tokens") + +# services/gmail_service.py +class FileCredentialsProvider: + def get_credentials(self, user_id: str): + token_path = self.token_base / f"{user_id}_gmail.json" + # ... 토큰 로드 +``` + +### 제안하는 DB 기반 구조 +```python +class DBCredentialsProvider: + async def get_credentials(self, user_id: str): + # PostgreSQL에서 토큰 조회 + token = await db.query( + "SELECT * FROM gmail_tokens WHERE user_id = %s", + user_id + ) + return Credentials(**token) +``` + +--- + +## 8. 다음 단계 + +1. **즉시 실행**: Mock 모드로 이메일 스킬 테스트 +2. **단기 과제**: Gmail OAuth 토큰 생성 및 저장 +3. **중기 과제**: PostgreSQL 기반 토큰 관리 시스템 +4. **장기 과제**: 멀티 로빙 및 사용자별 계정 관리 + +--- + +## 로그 예시 +``` +INFO: Calling service: http://172.17.0.1:8501/send + with payload keys: ['message', 'user_id', 'channel', 'robing_id', 'action', 'execution_plan'] +ERROR: 422 Unprocessable Entity +DETAIL: 토큰 파일이 없습니다: /app/auth-server/tokens/test_gmail.json +``` \ No newline at end of file diff --git a/slack_user_mapping_troubleshooting.md b/slack_user_mapping_troubleshooting.md new file mode 100644 index 0000000..66a116c --- /dev/null +++ b/slack_user_mapping_troubleshooting.md @@ -0,0 +1,141 @@ +# Slack User ID → 이름 매핑 문제 해결 + +## 문제점 +rb8001에서 Slack 사용자 ID (예: U0925SXQFDK)를 실제 이름으로 변환하지 못함 + +## 현재 상태 +1. **메모리 검색**: ✅ 정상 작동 + - ChromaDB에 22개 메모리 저장됨 + - search_memories() 함수 정상 작동 + - 과거 대화 검색 가능 + +2. **사용자 이름 매핑**: ❌ 미구현 + - slack_user_mapping 테이블 없음 + - Slack API를 통한 사용자 정보 조회 미구현 + +## 해결 방안 + +### 1. slack_user_mapping 테이블 생성 +```sql +CREATE TABLE slack_user_mapping ( + id SERIAL PRIMARY KEY, + slack_user_id VARCHAR(50) UNIQUE NOT NULL, + user_name VARCHAR(100), + display_name VARCHAR(100), + real_name VARCHAR(100), + email VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +### 2. Slack API로 사용자 정보 가져오기 +```python +from slack_sdk import WebClient + +class SlackUserService: + def __init__(self, slack_token): + self.client = WebClient(token=slack_token) + + async def get_user_info(self, user_id: str): + """Slack API로 사용자 정보 조회""" + try: + response = self.client.users_info(user=user_id) + if response["ok"]: + user = response["user"] + return { + "slack_user_id": user_id, + "user_name": user.get("name"), + "display_name": user["profile"].get("display_name"), + "real_name": user["profile"].get("real_name"), + "email": user["profile"].get("email") + } + except Exception as e: + logger.error(f"Failed to get user info: {e}") + return None +``` + +### 3. 캐싱 전략 +```python +class UserNameCache: + def __init__(self): + self.cache = {} # {user_id: user_info} + self.db = StateServiceClient() + + async def get_user_name(self, user_id: str) -> str: + # 1. 캐시 확인 + if user_id in self.cache: + return self.cache[user_id]["display_name"] + + # 2. DB 확인 + user_info = await self.db.get_user_mapping(user_id) + if user_info: + self.cache[user_id] = user_info + return user_info["display_name"] + + # 3. Slack API 호출 + slack_service = SlackUserService(settings.SLACK_BOT_TOKEN) + user_info = await slack_service.get_user_info(user_id) + + if user_info: + # DB에 저장 + await self.db.save_user_mapping(user_info) + self.cache[user_id] = user_info + return user_info["display_name"] + + return user_id # 실패 시 ID 반환 +``` + +### 4. 메시지 처리 시 이름 변환 +```python +# slack_handler.py +async def handle_message(event): + user_id = event.get("user") + + # 사용자 이름 가져오기 + user_name = await user_name_cache.get_user_name(user_id) + + # 메시지 처리 + message = event.get("text") + logger.info(f"Message from {user_name} ({user_id}): {message}") + + # 메모리 저장 시 이름 포함 + await memory_manager.add_memory( + content=f"{user_name}: {message}", + metadata={ + "user_id": user_id, + "user_name": user_name, + "channel_id": channel_id + } + ) +``` + +## 구현 우선순위 +1. **Phase 1**: 인메모리 캐시만 구현 + - Slack API로 실시간 조회 + - 메모리에 캐싱 + +2. **Phase 2**: DB 저장 추가 + - PostgreSQL에 slack_user_mapping 테이블 생성 + - 조회한 정보 영구 저장 + +3. **Phase 3**: 배치 업데이트 + - 주기적으로 사용자 정보 갱신 + - 변경사항 감지 + +## 테스트 시나리오 +```python +# 1. 새로운 사용자 메시지 +assert get_user_name("U0925SXQFDK") == "희재" + +# 2. 캐시된 사용자 +assert get_user_name("U0925SXQFDK") == "희재" # API 호출 없음 + +# 3. 알 수 없는 사용자 +assert get_user_name("UNKNOWN") == "UNKNOWN" +``` + +## 참고 사항 +- Slack API rate limit: 1분당 50회 +- users.info API는 user:read 스코프 필요 +- 사용자 정보는 변경될 수 있으므로 주기적 갱신 필요 \ No newline at end of file