이메일 진행 사항 저장
This commit is contained in:
parent
83bd125633
commit
c1bbaea09b
294
email_skill_integration_status.md
Normal file
294
email_skill_integration_status.md
Normal file
@ -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
|
||||
```
|
||||
141
slack_user_mapping_troubleshooting.md
Normal file
141
slack_user_mapping_troubleshooting.md
Normal file
@ -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 스코프 필요
|
||||
- 사용자 정보는 변경될 수 있으므로 주기적 갱신 필요
|
||||
Loading…
x
Reference in New Issue
Block a user