Ver3.5 LangChain으로 AI가 스스로 이메일 정리하기

LangChain으로 AI가 스스로 이메일 정리하기
야, 솔직히 말해봐. 📬 지금 이 순간 네 이메일 받은 편지함에 읽지 않은 메일이 몇 개야?
100개? 500개? 혹시 1,000개 넘어가는 거 아니야? 😅
현대인의 이메일 받은 편지함은 그야말로 디지털 정글이야. 업무 메일, 뉴스레터, 광고 메일, 스팸, 청구서, 회의 초대장... 이게 다 뒤섞여서 진짜 중요한 메일을 찾으려면 눈이 빠질 지경이지.
그런데 말이야, 이걸 AI가 알아서 척척 정리해준다면? 그것도 내가 원하는 방식으로, 내 취향에 맞게? 그게 바로 오늘 우리가 파헤칠 주제야.
LangChain을 이용해서 AI 에이전트를 만들고, 그 에이전트가 이메일을 읽고, 분류하고, 요약하고, 심지어 답장 초안까지 써주는 시스템을 구축하는 방법을 알아볼 거야. 프로그래밍 좀 해본 사람이라면 충분히 따라올 수 있어. 같이 가보자고! 🚀
LangChain을 모르는 사람을 위해 간단히 설명할게.
LangChain은 대형 언어 모델(LLM)을 활용한 애플리케이션을 쉽게 만들 수 있게 해주는 프레임워크야.
쉽게 말하면, ChatGPT 같은 AI를 내 앱에 붙여서 복잡한 작업을 시킬 수 있는 도구 모음이라고 보면 돼.
① LLM (Large Language Model)
GPT-4, Claude, Gemini 같은 거대 언어 모델. LangChain은 이것들을 쉽게 연결할 수 있어.
② Chain
여러 작업을 순서대로 연결하는 파이프라인. "이메일 읽기 → 분류 → 요약 → 저장" 이런 흐름을 만드는 거야.
③ Agent
스스로 판단해서 어떤 도구를 쓸지 결정하는 AI. 단순 체인보다 훨씬 똑똑하게 동작해.
④ Tool
에이전트가 사용할 수 있는 기능들. 이메일 읽기, 구글 캘린더 접근, 데이터베이스 저장 등이 다 Tool이 될 수 있어.
⑤ Memory
대화 맥락을 기억하는 기능. "아까 그 메일 말이야..." 이런 맥락 파악이 가능해져.
2023년에 등장해서 지금은 GitHub 스타 수가 90,000개 이상을 넘어선 초인기 오픈소스 프로젝트야. AI 개발 커뮤니티에서 사실상 표준 프레임워크로 자리잡았다고 봐도 무방해.
본격적으로 시스템을 만들기 전에, 전체 구조를 머릿속에 그려보자.
우리가 만들 시스템은 크게 이런 흐름으로 동작해:
1. 자동 분류 - 업무/개인/광고/스팸/뉴스레터 등으로 자동 분류
2. 우선순위 설정 - 긴급/중요/일반/낮음으로 우선순위 태깅
3. 요약 생성 - 긴 이메일을 3줄 요약으로 압축
4. 답장 초안 작성 - 맥락에 맞는 답장 초안 자동 생성
5. 중요 정보 추출 - 날짜, 금액, 링크, 연락처 등 핵심 정보 추출
6. 알림 발송 - 중요 메일은 슬랙/텔레그램으로 즉시 알림
자, 이제 진짜 코드를 써볼 시간이야. 먼저 필요한 것들을 설치하고 설정해보자.
# 기본 LangChain 패키지
pip install langchain langchain-openai langchain-community
# Gmail API 연동
pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client
# 데이터 처리
pip install python-dotenv pydantic
# 선택사항: 슬랙 알림
pip install slack-sdk
# .env 파일 생성
OPENAI_API_KEY=your_openai_api_key_here
GMAIL_CREDENTIALS_PATH=credentials.json
SLACK_BOT_TOKEN=your_slack_token_here # 선택사항
Gmail API를 사용하려면 Google Cloud Console에서 설정이 필요해. 순서대로 따라해봐:
이제 실제 코드를 작성해보자. 먼저 Gmail에서 이메일을 읽어오는 모듈부터 만들어.
import os
import base64
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from email.mime.text import MIMEText
from typing import List, Dict, Optional
# Gmail API 접근 권한 범위
SCOPES = [
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/gmail.modify',
'https://www.googleapis.com/auth/gmail.labels'
]
class GmailReader:
"""Gmail API를 통해 이메일을 읽고 관리하는 클래스"""
def __init__(self, credentials_path: str = 'credentials.json'):
self.credentials_path = credentials_path
self.service = self._authenticate()
def _authenticate(self):
"""Gmail API 인증 처리"""
creds = None
# 기존 토큰이 있으면 로드
if os.path.exists('token.json'):
creds = Credentials.from_authorized_user_file('token.json', SCOPES)
# 토큰이 없거나 만료된 경우
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
self.credentials_path, SCOPES
)
creds = flow.run_local_server(port=0)
# 토큰 저장
with open('token.json', 'w') as token:
token.write(creds.to_json())
return build('gmail', 'v1', credentials=creds)
def get_unread_emails(self, max_results: int = 20) -> List[Dict]:
"""읽지 않은 이메일 목록 가져오기"""
results = self.service.users().messages().list(
userId='me',
labelIds=['UNREAD'],
maxResults=max_results
).execute()
messages = results.get('messages', [])
emails = []
for msg in messages:
email_data = self._parse_email(msg['id'])
if email_data:
emails.append(email_data)
return emails
def _parse_email(self, msg_id: str) -> Optional[Dict]:
"""이메일 파싱 - 제목, 발신자, 본문 추출"""
try:
msg = self.service.users().messages().get(
userId='me',
id=msg_id,
format='full'
).execute()
headers = msg['payload']['headers']
# 헤더에서 정보 추출
subject = next(
(h['value'] for h in headers if h['name'] == 'Subject'),
'제목 없음'
)
sender = next(
(h['value'] for h in headers if h['name'] == 'From'),
'발신자 불명'
)
date = next(
(h['value'] for h in headers if h['name'] == 'Date'),
''
)
# 본문 추출
body = self._extract_body(msg['payload'])
return {
'id': msg_id,
'subject': subject,
'sender': sender,
'date': date,
'body': body[:3000], # 너무 길면 3000자로 제한
'snippet': msg.get('snippet', ''),
'labels': msg.get('labelIds', [])
}
except Exception as e:
print(f"이메일 파싱 오류 ({msg_id}): {e}")
return None
def _extract_body(self, payload: Dict) -> str:
"""이메일 본문 텍스트 추출"""
body = ""
if 'parts' in payload:
for part in payload['parts']:
if part['mimeType'] == 'text/plain':
data = part['body'].get('data', '')
if data:
body = base64.urlsafe_b64decode(data).decode('utf-8', errors='ignore')
break
elif payload['mimeType'] == 'text/plain':
data = payload['body'].get('data', '')
if data:
body = base64.urlsafe_b64decode(data).decode('utf-8', errors='ignore')
return body
def add_label(self, msg_id: str, label_name: str):
"""이메일에 라벨 추가"""
# 라벨 ID 가져오기 또는 생성
label_id = self._get_or_create_label(label_name)
self.service.users().messages().modify(
userId='me',
id=msg_id,
body={'addLabelIds': [label_id]}
).execute()
def _get_or_create_label(self, label_name: str) -> str:
"""라벨 ID 조회 또는 새 라벨 생성"""
labels = self.service.users().labels().list(userId='me').execute()
for label in labels.get('labels', []):
if label['name'] == label_name:
return label['id']
# 라벨이 없으면 새로 생성
new_label = self.service.users().labels().create(
userId='me',
body={'name': label_name}
).execute()
return new_label['id']
body[:3000]으로 본문을 3000자로 제한하는 이유는 LLM API 비용 때문이야. 이메일이 너무 길면 토큰 소비가 엄청나거든. 실제 운영할 때는 이 값을 적절히 조정해봐.
이제 진짜 핵심이야! LangChain을 이용해서 이메일을 분석하는 AI 에이전트를 만들어보자.
이 에이전트는 이메일을 읽고 분류, 요약, 우선순위 설정을 자동으로 해줄 거야.
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from typing import List, Optional
import os
from dotenv import load_dotenv
load_dotenv()
# 분석 결과 스키마 정의 (Pydantic 모델)
class EmailAnalysis(BaseModel):
"""이메일 분석 결과 구조"""
category: str = Field(
description="이메일 카테고리: 업무/개인/광고/스팸/뉴스레터/청구서/회의/기타 중 하나"
)
priority: str = Field(
description="우선순위: 긴급/중요/일반/낮음 중 하나"
)
summary: str = Field(
description="이메일 핵심 내용을 2-3문장으로 요약"
)
action_required: bool = Field(
description="답장이나 조치가 필요한지 여부"
)
key_info: List[str] = Field(
description="날짜, 금액, 링크, 연락처 등 중요 정보 목록",
default_factory=list
)
suggested_reply: Optional[str] = Field(
description="답장이 필요한 경우 간단한 답장 초안 (필요 없으면 null)",
default=None
)
sentiment: str = Field(
description="이메일 감정 톤: 긍정/중립/부정/긴급 중 하나"
)
class EmailAnalyzer:
"""LangChain을 이용한 이메일 분석기"""
def __init__(self, model: str = "gpt-4o-mini"):
self.llm = ChatOpenAI(
model=model,
temperature=0, # 일관된 분류를 위해 temperature=0
api_key=os.getenv("OPENAI_API_KEY")
)
self.parser = PydanticOutputParser(pydantic_object=EmailAnalysis)
self.chain = self._build_chain()
def _build_chain(self):
"""분석 체인 구성"""
prompt = ChatPromptTemplate.from_messages([
("system", """당신은 전문적인 이메일 분석 AI 어시스턴트입니다.
주어진 이메일을 분석하여 정확하고 유용한 정보를 추출해주세요.
분류 기준:
- 업무: 회사 관련, 프로젝트, 동료/클라이언트 메일
- 개인: 친구, 가족, 개인적인 내용
- 광고: 마케팅, 프로모션, 할인 안내
- 스팸: 의심스러운 내용, 피싱 시도
- 뉴스레터: 정기 구독 콘텐츠
- 청구서: 결제, 영수증, 인보이스
- 회의: 미팅 초대, 일정 관련
- 기타: 위 카테고리에 해당하지 않는 것
우선순위 기준:
- 긴급: 즉시 처리 필요, 마감 임박
- 중요: 중요하지만 당일 처리 가능
- 일반: 일반적인 업무 메일
- 낮음: 나중에 봐도 되는 메일
{format_instructions}"""),
("human", """다음 이메일을 분석해주세요:
발신자: {sender}
제목: {subject}
날짜: {date}
본문:
{body}""")
])
return prompt | self.llm | self.parser
def analyze(self, email_data: dict) -> EmailAnalysis:
"""이메일 분석 실행"""
try:
result = self.chain.invoke({
"sender": email_data.get('sender', ''),
"subject": email_data.get('subject', ''),
"date": email_data.get('date', ''),
"body": email_data.get('body', email_data.get('snippet', '')),
"format_instructions": self.parser.get_format_instructions()
})
return result
except Exception as e:
print(f"분석 오류: {e}")
# 오류 시 기본값 반환
return EmailAnalysis(
category="기타",
priority="일반",
summary="분석 중 오류가 발생했습니다.",
action_required=False,
key_info=[],
suggested_reply=None,
sentiment="중립"
)
def batch_analyze(self, emails: list) -> list:
"""여러 이메일 일괄 분석"""
results = []
for i, email in enumerate(emails):
print(f"분석 중... ({i+1}/{len(emails)}): {email.get('subject', '')[:30]}")
analysis = self.analyze(email)
results.append({
'email': email,
'analysis': analysis
})
return results
LLM의 출력은 기본적으로 자유로운 텍스트야. 근데 우리는 구조화된 데이터가 필요하잖아.
Pydantic 모델을 PydanticOutputParser와 함께 쓰면 LLM이 자동으로 우리가 원하는 형식으로 출력을 만들어줘.
예를 들어 category는 반드시 "업무/개인/광고/스팸/뉴스레터/청구서/회의/기타" 중 하나여야 한다고 강제할 수 있어. 이게 없으면 LLM이 "이 메일은 업무 관련 메일로 보입니다..." 이런 식으로 자유롭게 써버려서 파싱이 힘들어져.
위에서 만든 건 단순한 체인(Chain)이야. 이제 한 단계 업그레이드해서 Agent를 만들어보자.
에이전트는 상황에 따라 어떤 도구를 쓸지 스스로 판단해. 훨씬 유연하고 강력해!
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain.tools import tool
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.memory import ConversationBufferMemory
from langchain_core.messages import SystemMessage
from gmail_reader import GmailReader
from email_analyzer import EmailAnalyzer
import json
import os
from dotenv import load_dotenv
load_dotenv()
# Gmail 리더와 분석기 초기화
gmail = GmailReader()
analyzer = EmailAnalyzer()
# ===== 에이전트가 사용할 도구(Tool) 정의 =====
@tool
def get_unread_emails(max_count: int = 10) -> str:
"""읽지 않은 이메일 목록을 가져옵니다.
Args:
max_count: 가져올 최대 이메일 수 (기본값: 10)
Returns:
이메일 목록 JSON 문자열
"""
emails = gmail.get_unread_emails(max_results=max_count)
# 간략한 정보만 반환 (토큰 절약)
simplified = []
for email in emails:
simplified.append({
'id': email['id'],
'subject': email['subject'],
'sender': email['sender'],
'date': email['date'],
'snippet': email['snippet'][:200]
})
return json.dumps(simplified, ensure_ascii=False, indent=2)
@tool
def analyze_email(email_id: str) -> str:
"""특정 이메일을 AI로 분석합니다.
Args:
email_id: 분석할 이메일의 ID
Returns:
분석 결과 JSON 문자열
"""
emails = gmail.get_unread_emails(max_results=50)
target_email = next((e for e in emails if e['id'] == email_id), None)
if not target_email:
return json.dumps({"error": "이메일을 찾을 수 없습니다."})
analysis = analyzer.analyze(target_email)
return json.dumps({
'email_id': email_id,
'subject': target_email['subject'],
'category': analysis.category,
'priority': analysis.priority,
'summary': analysis.summary,
'action_required': analysis.action_required,
'key_info': analysis.key_info,
'suggested_reply': analysis.suggested_reply,
'sentiment': analysis.sentiment
}, ensure_ascii=False, indent=2)
@tool
def label_email(email_id: str, label: str) -> str:
"""이메일에 라벨을 추가합니다.
Args:
email_id: 라벨을 추가할 이메일 ID
label: 추가할 라벨 이름
Returns:
처리 결과 메시지
"""
try:
gmail.add_label(email_id, f"AI분류/{label}")
return f"이메일 {email_id}에 라벨 'AI분류/{label}'을 추가했습니다."
except Exception as e:
return f"라벨 추가 실패: {str(e)}"
@tool
def get_email_summary_report() -> str:
"""전체 읽지 않은 이메일의 요약 리포트를 생성합니다.
Returns:
전체 이메일 현황 리포트
"""
emails = gmail.get_unread_emails(max_results=20)
results = analyzer.batch_analyze(emails)
# 카테고리별 집계
category_count = {}
priority_count = {}
urgent_emails = []
for item in results:
analysis = item['analysis']
email = item['email']
category_count[analysis.category] = category_count.get(analysis.category, 0) + 1
priority_count[analysis.priority] = priority_count.get(analysis.priority, 0) + 1
if analysis.priority == '긴급':
urgent_emails.append({
'subject': email['subject'],
'sender': email['sender'],
'summary': analysis.summary
})
report = {
'총_이메일_수': len(emails),
'카테고리별_현황': category_count,
'우선순위별_현황': priority_count,
'긴급_이메일': urgent_emails
}
return json.dumps(report, ensure_ascii=False, indent=2)
# ===== 에이전트 구성 =====
def create_email_agent():
"""이메일 관리 에이전트 생성"""
llm = ChatOpenAI(
model="gpt-4o",
temperature=0,
api_key=os.getenv("OPENAI_API_KEY")
)
tools = [
get_unread_emails,
analyze_email,
label_email,
get_email_summary_report
]
prompt = ChatPromptTemplate.from_messages([
SystemMessage(content="""당신은 전문적인 이메일 관리 AI 어시스턴트입니다.
사용자의 이메일을 효율적으로 관리하고 정리하는 것이 당신의 역할입니다.
주요 역할:
1. 읽지 않은 이메일 현황 파악
2. 이메일 분류 및 우선순위 설정
3. 중요 이메일 요약 제공
4. 답장이 필요한 이메일 식별
5. 라벨링을 통한 자동 정리
항상 한국어로 응답하고, 사용자가 빠르게 이메일 상황을 파악할 수 있도록 도와주세요."""),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad")
])
agent = create_openai_tools_agent(llm, tools, prompt)
memory = ConversationBufferMemory(
memory_key="chat_history",
return_messages=True
)
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
memory=memory,
verbose=True,
max_iterations=10
)
return agent_executor
# 에이전트 실행 예시
if __name__ == "__main__":
agent = create_email_agent()
# 전체 이메일 현황 리포트 요청
response = agent.invoke({
"input": "오늘 받은 이메일 현황을 분석해서 요약 리포트를 만들어줘. 긴급한 것들은 특히 강조해줘."
})
print(response['output'])
Agent는 상황을 보고 스스로 판단해. "이 요청엔 먼저 이메일 목록을 가져오고, 그 다음 분석하고, 그 다음 라벨링을 해야겠다" 이런 식으로 동적으로 결정해. 훨씬 유연하지!
에이전트를 만들었으면 이제 이걸 자동으로 실행되게 해야지!
새 이메일이 올 때마다 자동으로 분석하고 정리하는 스케줄러를 만들어보자.
import schedule
import time
import json
from datetime import datetime
from gmail_reader import GmailReader
from email_analyzer import EmailAnalyzer, EmailAnalysis
from typing import List, Dict
import logging
# 로깅 설정
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('email_bot.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
class EmailAutomationBot:
"""이메일 자동화 봇"""
def __init__(self):
self.gmail = GmailReader()
self.analyzer = EmailAnalyzer()
self.processed_ids = self._load_processed_ids()
logger.info("이메일 자동화 봇 시작!")
def _load_processed_ids(self) -> set:
"""이미 처리한 이메일 ID 로드 (중복 처리 방지)"""
try:
with open('processed_emails.json', 'r') as f:
return set(json.load(f))
except FileNotFoundError:
return set()
def _save_processed_ids(self):
"""처리한 이메일 ID 저장"""
with open('processed_emails.json', 'w') as f:
json.dump(list(self.processed_ids), f)
def process_new_emails(self):
"""새 이메일 처리 메인 함수"""
logger.info("새 이메일 확인 중...")
try:
emails = self.gmail.get_unread_emails(max_results=20)
new_emails = [e for e in emails if e['id'] not in self.processed_ids]
if not new_emails:
logger.info("새 이메일 없음")
return
logger.info(f"새 이메일 {len(new_emails)}개 발견!")
for email in new_emails:
self._process_single_email(email)
self.processed_ids.add(email['id'])
self._save_processed_ids()
except Exception as e:
logger.error(f"이메일 처리 오류: {e}")
def _process_single_email(self, email: Dict):
"""단일 이메일 처리"""
logger.info(f"처리 중: {email['subject'][:50]}")
# AI 분석
analysis = self.analyzer.analyze(email)
# 라벨 추가
label = f"AI분류/{analysis.category}"
priority_label = f"AI우선순위/{analysis.priority}"
try:
self.gmail.add_label(email['id'], label)
self.gmail.add_label(email['id'], priority_label)
except Exception as e:
logger.warning(f"라벨 추가 실패: {e}")
# 결과 로깅
logger.info(f"""
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📧 제목: {email['subject']}
👤 발신자: {email['sender']}
🏷️ 카테고리: {analysis.category}
⭐ 우선순위: {analysis.priority}
📝 요약: {analysis.summary}
🔔 조치 필요: {'예' if analysis.action_required else '아니오'}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
""")
# 긴급 이메일은 즉시 알림
if analysis.priority == '긴급':
self._send_urgent_notification(email, analysis)
def _send_urgent_notification(self, email: Dict, analysis: EmailAnalysis):
"""긴급 이메일 알림 발송 (슬랙 또는 콘솔)"""
message = f"""
🚨 긴급 이메일 알림!
━━━━━━━━━━━━━━━━━━━━
📧 제목: {email['subject']}
👤 발신자: {email['sender']}
📝 요약: {analysis.summary}
🔑 핵심 정보: {', '.join(analysis.key_info) if analysis.key_info else '없음'}
━━━━━━━━━━━━━━━━━━━━
"""
# 슬랙 알림 (설정된 경우)
slack_token = os.getenv('SLACK_BOT_TOKEN')
if slack_token:
self._send_slack_notification(message, slack_token)
else:
# 콘솔 출력
print(message)
def _send_slack_notification(self, message: str, token: str):
"""슬랙으로 알림 발송"""
try:
from slack_sdk import WebClient
client = WebClient(token=token)
client.chat_postMessage(
channel="#이메일-알림",
text=message
)
logger.info("슬랙 알림 발송 완료")
except Exception as e:
logger.error(f"슬랙 알림 실패: {e}")
def generate_daily_report(self):
"""일일 이메일 현황 리포트 생성"""
logger.info("일일 리포트 생성 중...")
emails = self.gmail.get_unread_emails(max_results=50)
results = self.analyzer.batch_analyze(emails)
# 통계 집계
stats = {
'total': len(emails),
'categories': {},
'priorities': {},
'action_required': 0,
'urgent': []
}
for item in results:
a = item['analysis']
e = item['email']
stats['categories'][a.category] = stats['categories'].get(a.category, 0) + 1
stats['priorities'][a.priority] = stats['priorities'].get(a.priority, 0) + 1
if a.action_required:
stats['action_required'] += 1
if a.priority == '긴급':
stats['urgent'].append(f"- {e['subject'][:40]} ({e['sender'][:30]})")
report = f"""
📊 일일 이메일 현황 리포트 ({datetime.now().strftime('%Y-%m-%d %H:%M')})
{'='*50}
📬 총 미읽음 이메일: {stats['total']}개
✅ 조치 필요: {stats['action_required']}개
📁 카테고리별:
{chr(10).join([f" • {k}: {v}개" for k, v in stats['categories'].items()])}
⭐ 우선순위별:
{chr(10).join([f" • {k}: {v}개" for k, v in stats['priorities'].items()])}
🚨 긴급 이메일:
{chr(10).join(stats['urgent']) if stats['urgent'] else ' 없음'}
{'='*50}
"""
print(report)
logger.info("일일 리포트 생성 완료")
return report
# 스케줄러 실행
def run_scheduler():
bot = EmailAutomationBot()
# 5분마다 새 이메일 확인
schedule.every(5).minutes.do(bot.process_new_emails)
# 매일 오전 9시에 일일 리포트 생성
schedule.every().day.at("09:00").do(bot.generate_daily_report)
logger.info("스케줄러 시작! (Ctrl+C로 종료)")
# 시작 시 즉시 한 번 실행
bot.process_new_emails()
while True:
schedule.run_pending()
time.sleep(60)
if __name__ == "__main__":
run_scheduler()
여기서 한 단계 더 나아가보자. RAG(Retrieval-Augmented Generation)를 활용하면 과거 이메일 패턴을 학습해서 더 정확한 분류가 가능해져.
예를 들어, "이 발신자는 항상 업무 관련 메일을 보내는 사람이야"라는 컨텍스트를 AI에게 제공할 수 있어.
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
import json
import os
class EmailRAGSystem:
"""과거 이메일 패턴을 학습하는 RAG 시스템"""
def __init__(self, persist_directory: str = "./email_vectordb"):
self.embeddings = OpenAIEmbeddings(
api_key=os.getenv("OPENAI_API_KEY")
)
self.persist_directory = persist_directory
self.vectorstore = self._init_vectorstore()
def _init_vectorstore(self):
"""벡터 저장소 초기화"""
if os.path.exists(self.persist_directory):
# 기존 벡터 DB 로드
return Chroma(
persist_directory=self.persist_directory,
embedding_function=self.embeddings
)
else:
# 새 벡터 DB 생성
return Chroma(
persist_directory=self.persist_directory,
embedding_function=self.embeddings
)
def add_email_to_knowledge_base(self, email: dict, analysis_result: dict):
"""처리된 이메일을 지식 베이스에 추가"""
# 이메일 + 분석 결과를 텍스트로 변환
document_text = f"""
발신자: {email.get('sender', '')}
제목: {email.get('subject', '')}
카테고리: {analysis_result.get('category', '')}
우선순위: {analysis_result.get('priority', '')}
요약: {analysis_result.get('summary', '')}
본문 일부: {email.get('body', '')[:500]}
"""
# 메타데이터 저장
metadata = {
'sender': email.get('sender', ''),
'category': analysis_result.get('category', ''),
'priority': analysis_result.get('priority', ''),
'email_id': email.get('id', '')
}
self.vectorstore.add_texts(
texts=[document_text],
metadatas=[metadata]
)
def get_similar_emails_context(self, new_email: dict, k: int = 3) -> str:
"""새 이메일과 유사한 과거 이메일 패턴 검색"""
query = f"{new_email.get('sender', '')} {new_email.get('subject', '')}"
similar_docs = self.vectorstore.similarity_search(query, k=k)
if not similar_docs:
return "관련 과거 이메일 패턴 없음"
context = "유사한 과거 이메일 패턴:\n"
for i, doc in enumerate(similar_docs, 1):
context += f"\n[패턴 {i}]\n{doc.page_content}\n"
return context
def analyze_with_context(self, email: dict, analyzer) -> dict:
"""과거 패턴을 참고한 향상된 분석"""
# 유사 패턴 검색
context = self.get_similar_emails_context(email)
# 컨텍스트를 포함한 이메일 데이터 생성
enhanced_email = email.copy()
enhanced_email['body'] = f"""
[과거 패턴 참고 정보]
{context}
[현재 이메일 본문]
{email.get('body', '')}
"""
# 분석 실행
analysis = analyzer.analyze(enhanced_email)
# 결과를 지식 베이스에 추가
self.add_email_to_knowledge_base(email, {
'category': analysis.category,
'priority': analysis.priority,
'summary': analysis.summary
})
return analysis
상황 1: 특정 클라이언트가 항상 긴급한 메일을 보내는 패턴이 있다면, RAG가 이를 기억해서 다음 메일도 자동으로 높은 우선순위로 분류해줘.
상황 2: 특정 도메인(@newsletter.com 같은)에서 오는 메일은 항상 뉴스레터로 분류되는 패턴을 학습해.
상황 3: 내가 자주 답장하는 발신자의 메일은 더 높은 우선순위로 설정하는 패턴을 학습해.
시간이 지날수록 점점 더 나에게 맞춤화된 분류가 이루어지는 거야! 🎉
이제 마지막 단계야. 터미널에서 AI와 대화하면서 이메일을 관리할 수 있는 대화형 인터페이스를 만들어보자.
from email_agent import create_email_agent
import sys
def run_chat_interface():
"""대화형 이메일 관리 인터페이스"""
print("""
╔══════════════════════════════════════╗
║ 📧 AI 이메일 어시스턴트 v1.0 ║
║ LangChain + GPT-4 기반 ║
║ 'quit' 입력 시 종료 ║
╚══════════════════════════════════════╝
""")
agent = create_email_agent()
# 시작 메시지
print("안녕하세요! 이메일 관리를 도와드릴게요.")
print("예시 명령어:")
print(" - '오늘 받은 이메일 현황 알려줘'")
print(" - '긴급한 이메일 있어?'")
print(" - '광고 메일 다 정리해줘'")
print(" - '이메일 요약 리포트 만들어줘'")
print("-" * 40)
while True:
try:
user_input = input("\n🙋 나: ").strip()
if not user_input:
continue
if user_input.lower() in ['quit', 'exit', '종료', '끝']:
print("👋 이메일 어시스턴트를 종료합니다. 수고하셨어요!")
break
print("\n🤖 AI: 처리 중...")
response = agent.invoke({"input": user_input})
print(f"\n🤖 AI: {response['output']}")
except KeyboardInterrupt:
print("\n\n👋 종료합니다!")
break
except Exception as e:
print(f"\n❌ 오류 발생: {e}")
print("다시 시도해주세요.")
if __name__ == "__main__":
run_chat_interface()
이렇게 하면 터미널에서 AI와 대화하면서 이메일을 관리할 수 있어!
"오늘 긴급한 이메일 있어?" 라고 물으면 AI가 알아서 이메일을 분석하고 답해줘.
이론은 다 좋은데, 실제로 쓰다 보면 비용이 문제가 될 수 있어.
GPT-4를 이메일 하나하나에 다 쓰면 요금이 꽤 나올 수 있거든. 최적화 팁을 알려줄게!
| 최적화 방법 | 효과 | 구현 난이도 |
|---|---|---|
| GPT-4o-mini 사용 | 비용 약 90% 절감 | ⭐ 쉬움 |
| 본문 길이 제한 (3000자) | 토큰 50% 절감 | ⭐ 쉬움 |
| 배치 처리 | API 호출 횟수 감소 | ⭐⭐ 보통 |
| 캐싱 적용 | 동일 메일 재분석 방지 | ⭐⭐ 보통 |
| 로컬 LLM (Ollama) | API 비용 0원 | ⭐⭐⭐ 어려움 |
| 스팸 사전 필터링 | 불필요한 분석 제거 | ⭐ 쉬움 |
GPT-4o-mini 기준 (2024년 기준):
• 입력: $0.15 / 1M 토큰
• 출력: $0.60 / 1M 토큰
이메일 1개 분석 시 약 1,500~2,000 토큰 사용
→ 이메일 1개당 약 $0.0003~0.0005 (약 0.4~0.7원)
하루 100개 이메일 처리 시 → 약 40~70원/일
한 달 기준 → 약 1,200~2,100원/월
이 정도면 충분히 합리적이지 않아? 😄
이메일은 매우 민감한 정보야. 보안에 대해 꼭 짚고 넘어가야 해!
① API 키 관리
.env 파일 사용, .gitignore에 추가 필수. 절대 코드에 하드코딩 금지!
② OAuth 2.0 사용
Gmail 비밀번호를 직접 쓰지 말고 반드시 OAuth 2.0 인증 사용.
③ 최소 권한 원칙
읽기만 필요하면 readonly 권한만 요청. 불필요한 권한 요청 금지.
④ 이메일 내용 외부 전송 주의
이메일 본문이 OpenAI 서버로 전송된다는 점 인지. 기밀 정보가 많다면 로컬 LLM 고려.
⑤ 토큰 파일 보호
token.json 파일도 .gitignore에 추가. 이 파일이 유출되면 이메일 접근 가능!
⑥ 로그 파일 관리
로그에 이메일 내용이 기록될 수 있으니 로그 파일도 안전하게 관리.
기본 시스템을 만들었으면 이제 더 재미있는 기능들을 추가해볼 수 있어!
① 감정 분석 기반 우선순위
발신자의 감정 톤이 부정적이거나 긴박하면 자동으로 우선순위 상향
② 스레드 분석
이메일 스레드 전체를 분석해서 대화 흐름 파악 및 요약
③ 첨부파일 분석
PDF, 이미지 첨부파일 내용까지 AI로 분석 (GPT-4 Vision 활용)
④ 개인화 학습
내가 직접 분류한 이메일 패턴을 학습해서 점점 더 정확해지는 시스템
⑤ 다중 이메일 계정 관리
Gmail, Outlook, 네이버 메일 등 여러 계정을 하나의 AI로 통합 관리
이런 아이디어들을 구현하고 싶다면, 재능넷(https://www.jaenung.net) 같은 플랫폼에서 AI 개발 전문가를 찾아서 협업하는 것도 좋은 방법이야. 혼자 다 하기 어려운 복잡한 기능들은 전문가의 도움을 받으면 훨씬 빠르게 구현할 수 있거든! 😊
실제로 개발하다 보면 이런 오류들을 자주 만나게 될 거야. 미리 알아두면 시간 절약!
| 오류 | 원인 | 해결법 |
|---|---|---|
AuthenticationError |
API 키 오류 | .env 파일 확인, 키 재발급 |
RateLimitError |
API 호출 한도 초과 | 배치 처리 간격 늘리기, 지수 백오프 적용 |
OutputParserException |
LLM 출력 형식 오류 | 프롬프트 개선, retry 로직 추가 |
HttpError 403 |
Gmail 권한 부족 | OAuth 스코프 확인, 재인증 |
ContextLengthExceeded |
입력 토큰 초과 | 본문 길이 제한 줄이기 |
# 지수 백오프(Exponential Backoff) 적용 예시
import time
import random
from functools import wraps
def retry_with_backoff(max_retries=3, base_delay=1):
"""API 오류 시 자동 재시도 데코레이터"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_retries - 1:
raise e
# 지수 백오프 + 랜덤 지터
delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
print(f"오류 발생, {delay:.1f}초 후 재시도... ({attempt+1}/{max_retries})")
time.sleep(delay)
return wrapper
return decorator
# 사용 예시
@retry_with_backoff(max_retries=3, base_delay=2)
def analyze_email_with_retry(analyzer, email):
return analyzer.analyze(email)
이런 시스템을 실제로 구축하면 어떤 효과가 있을까? 실제 사용 사례를 기반으로 정리해봤어.
상황: 하루 평균 50~80개의 이메일 수신, 클라이언트 문의, 견적 요청, 청구서 등 다양
적용 후:
• 이메일 확인 시간 하루 2시간 → 30분으로 단축
• 긴급 클라이언트 문의 응답 시간 평균 4시간 → 30분으로 단축
• 놓쳤던 중요 이메일 0건 (이전엔 주 2~3건 놓침)
• 월 절약 시간: 약 45시간
상황: 투자자, 파트너사, 팀원, 고객 등 다양한 이해관계자로부터 하루 100개 이상 이메일
적용 후:
• 투자자 이메일 자동 최우선 분류로 응답 속도 80% 향상
• 뉴스레터/광고 자동 필터링으로 받은 편지함 노이즈 70% 감소
• 일일 이메일 요약 리포트로 아침 브리핑 시간 15분으로 단축
상황: 논문 리뷰 요청, 학회 안내, 학생 문의, 협업 제안 등 다양한 이메일
적용 후:
• 논문 리뷰 마감일 자동 추출 및 캘린더 등록으로 마감 놓침 0건
• 학생 문의 FAQ 패턴 학습으로 반복 답장 자동화 60%
• 협업 제안 이메일 자동 분류로 중요 기회 놓침 방지
이런 시스템을 직접 구축하기 어렵다면, 재능넷에서 AI 개발 전문가를 찾아 의뢰하는 것도 좋은 선택이야. 맞춤형 이메일 자동화 시스템을 전문가와 함께 만들면 훨씬 빠르고 안정적으로 구축할 수 있거든!
지금까지 만든 모든 파일을 정리해보면 이런 구조야:
email-ai-assistant/
│
├── .env # API 키 등 환경변수 (git 제외!)
├── .gitignore # .env, token.json 등 제외
├── credentials.json # Gmail OAuth 자격증명 (git 제외!)
├── token.json # Gmail 인증 토큰 (자동 생성, git 제외!)
│
├── gmail_reader.py # Gmail API 연동 모듈
├── email_analyzer.py # LangChain 이메일 분석기
├── email_agent.py # LangChain 에이전트
├── email_rag.py # RAG 시스템 (과거 패턴 학습)
├── email_scheduler.py # 자동화 스케줄러
├── chat_interface.py # 대화형 인터페이스
│
├── processed_emails.json # 처리된 이메일 ID 목록 (자동 생성)
├── email_bot.log # 실행 로그 (자동 생성)
├── email_vectordb/ # RAG 벡터 DB (자동 생성)
│
└── requirements.txt # 의존성 패키지 목록
# requirements.txt
langchain==0.3.0
langchain-openai==0.2.0
langchain-community==0.3.0
google-auth==2.35.0
google-auth-oauthlib==1.2.1
google-auth-httplib2==0.2.0
google-api-python-client==2.149.0
python-dotenv==1.0.1
pydantic==2.9.2
schedule==1.2.2
chromadb==0.5.15
slack-sdk==3.33.0
자, 오늘 우리가 함께 만든 것들을 돌아보자. 🎉
LangChain을 이용해서:
이게 단순한 이메일 정리 도구가 아니야. 이건 AI가 나의 업무 패턴을 이해하고, 나 대신 판단하고, 나의 시간을 아껴주는 개인 비서야.
LangChain의 진짜 매력은 여기서 끝이 아니야. 이 시스템에 웹 검색 도구를 붙이면 이메일에서 언급된 회사를 자동으로 검색하고, 구글 캘린더 도구를 붙이면 회의 일정을 자동으로 등록하고, Notion 도구를 붙이면 중요 내용을 자동으로 노트에 저장하는 것도 가능해.
AI 개발의 세계에서 상상력이 곧 한계야. 오늘 배운 내용을 기반으로 너만의 스마트한 이메일 어시스턴트를 만들어봐. 처음엔 작게 시작해도 돼. 분류 기능 하나만 먼저 만들어보고, 점점 기능을 추가해나가면 어느새 완성된 시스템이 되어 있을 거야! 💪
궁금한 점이 있으면 언제든지 물어봐. 같이 AI 개발의 재미를 즐겨보자고! 🚀
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.