feat : initial commit
This commit is contained in:
commit
00eae37abc
28
.env.example
Normal file
28
.env.example
Normal file
@ -0,0 +1,28 @@
|
||||
# .env.example
|
||||
# HealthSync Motivator Batch 환경설정 샘플
|
||||
|
||||
# 애플리케이션 설정
|
||||
APP_NAME=HealthSync Motivator Batch
|
||||
APP_VERSION=1.0.0
|
||||
DEBUG=True
|
||||
|
||||
# PostgreSQL 데이터베이스 설정
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=healthsync_ai
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=your_database_password_here
|
||||
|
||||
# Claude AI API 설정
|
||||
CLAUDE_API_KEY=your_claude_api_key_here
|
||||
CLAUDE_MODEL=claude-3-5-sonnet-20241022
|
||||
CLAUDE_MAX_TOKENS=150
|
||||
CLAUDE_TEMPERATURE=0.7
|
||||
CLAUDE_TIMEOUT=30
|
||||
|
||||
# 배치 설정
|
||||
BATCH_SIZE=100
|
||||
MAX_RETRIES=3
|
||||
|
||||
# 로깅 설정
|
||||
LOG_LEVEL=INFO
|
||||
56
.gitignore
vendored
Normal file
56
.gitignore
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.venv/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite3
|
||||
healthsync_ai.db
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# HealthSync AI specific
|
||||
temp/
|
||||
uploads/
|
||||
cache/
|
||||
27
Dockerfile
Normal file
27
Dockerfile
Normal file
@ -0,0 +1,27 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# 작업 디렉토리 설정
|
||||
WORKDIR /app
|
||||
|
||||
# 시스템 패키지 업데이트 및 필수 패키지 설치
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Python 의존성 설치
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 애플리케이션 코드 복사
|
||||
COPY app/ ./app/
|
||||
|
||||
# 실행 권한 부여
|
||||
RUN chmod +x app/batch_runner.py
|
||||
|
||||
# 환경변수 설정
|
||||
ENV PYTHONPATH=/app
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# 기본 명령어 (배치 실행)
|
||||
CMD ["python", "app/batch_runner.py"]
|
||||
40
README.md
Normal file
40
README.md
Normal file
@ -0,0 +1,40 @@
|
||||
# HealthSync Motivator Batch Service
|
||||
|
||||
AI 기반 미션 독려 메시지 생성 배치 서비스
|
||||
|
||||
## 🎯 서비스 개요
|
||||
|
||||
사용자가 완료하지 않은 건강 미션을 조회하여 Claude AI를 통해 개인화된 독려 메시지를 생성하고, 채팅 DB에 저장하는 배치 서비스입니다.
|
||||
|
||||
## 🏗️ 아키텍처
|
||||
```
|
||||
📁 motivator-batch-service/
|
||||
├── app/
|
||||
│ ├── batch_runner.py # 메인 실행파일 (크론탭용)
|
||||
│ ├── config/ # 환경설정
|
||||
│ ├── models/ # 데이터 모델
|
||||
│ ├── services/ # 비즈니스 로직
|
||||
│ ├── repositories/ # 데이터베이스 쿼리
|
||||
│ └── utils/ # 유틸리티
|
||||
```
|
||||
|
||||
## 🔄 배치 처리 플로우
|
||||
|
||||
1. **활성 사용자 조회**: 최근 30일 내 로그인한 사용자
|
||||
2. **미완료 미션 조회**: 오늘 완료되지 않은 미션 목록
|
||||
3. **독려 메시지 생성**: Claude AI를 통한 개인화된 메시지 생성
|
||||
4. **채팅 DB 저장**: `intelligence_service.chat_message` 테이블에 저장
|
||||
|
||||
## 🚀 실행 방법
|
||||
|
||||
### 로컬 실행
|
||||
```bash
|
||||
# 환경설정 파일 생성
|
||||
cp .env.example .env
|
||||
# .env 파일 수정 (API 키, DB 정보 등)
|
||||
|
||||
# 의존성 설치
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 배치 실행
|
||||
python app/batch_runner.py
|
||||
7
app/__init__.py
Normal file
7
app/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
"""
|
||||
HealthSync AI Motivator Batch Service
|
||||
독려 메시지 생성 배치 서비스
|
||||
"""
|
||||
__version__ = "1.0.0"
|
||||
__title__ = "HealthSync Motivator Batch"
|
||||
__description__ = "AI 기반 미션 독려 메시지 생성 배치 서비스"
|
||||
71
app/batch_runner.py
Normal file
71
app/batch_runner.py
Normal file
@ -0,0 +1,71 @@
|
||||
# app/batch_runner.py
|
||||
"""
|
||||
HealthSync Motivator Batch 메인 실행파일 (크론탭용)
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from app.config.settings import settings
|
||||
from app.utils.database_utils import db
|
||||
from app.utils.logger import batch_logger
|
||||
from app.services.motivation_service import motivation_service
|
||||
|
||||
|
||||
async def main():
|
||||
"""메인 배치 실행 함수"""
|
||||
start_time = datetime.now()
|
||||
batch_logger.info(f"🚀 {settings.app_name} 배치 시작 - {start_time}")
|
||||
|
||||
try:
|
||||
# 1. 데이터베이스 연결 테스트
|
||||
batch_logger.info("🔍 데이터베이스 연결 테스트...")
|
||||
connection_ok = await db.test_connection()
|
||||
if not connection_ok:
|
||||
raise Exception("데이터베이스 연결 실패")
|
||||
batch_logger.info("✅ 데이터베이스 연결 성공")
|
||||
|
||||
# 2. 독려 메시지 배치 처리 실행
|
||||
batch_logger.info("💌 독려 메시지 배치 처리 시작...")
|
||||
results = await motivation_service.process_all_users()
|
||||
|
||||
# 3. 처리 결과 요약
|
||||
end_time = datetime.now()
|
||||
duration = end_time - start_time
|
||||
|
||||
batch_logger.info(f"🎉 배치 처리 완료!")
|
||||
batch_logger.info(f"📊 처리 결과:")
|
||||
batch_logger.info(f" - 전체 사용자: {results['total_users']}명")
|
||||
batch_logger.info(f" - 처리 완료: {results['processed_users']}명")
|
||||
batch_logger.info(f" - 메시지 발송: {results['sent_messages']}건")
|
||||
batch_logger.info(f" - 건너뛴 사용자: {results['skipped_users']}명")
|
||||
batch_logger.info(f" - 실패한 사용자: {results['failed_users']}명")
|
||||
batch_logger.info(f"⏱️ 총 소요시간: {duration}")
|
||||
|
||||
# 4. 성공 종료
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
batch_logger.error(f"❌ 배치 처리 실패: {str(e)}")
|
||||
return 1
|
||||
|
||||
finally:
|
||||
# 5. 데이터베이스 연결 해제
|
||||
try:
|
||||
await db.disconnect()
|
||||
batch_logger.info("🔌 데이터베이스 연결 해제 완료")
|
||||
except Exception as e:
|
||||
batch_logger.error(f"❌ 데이터베이스 연결 해제 실패: {str(e)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""크론탭에서 실행되는 엔트리포인트"""
|
||||
try:
|
||||
# 비동기 메인 함수 실행
|
||||
exit_code = asyncio.run(main())
|
||||
sys.exit(exit_code)
|
||||
except KeyboardInterrupt:
|
||||
batch_logger.info("⚠️ 사용자에 의한 배치 중단")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
batch_logger.error(f"❌ 배치 실행 중 예상치 못한 오류: {str(e)}")
|
||||
sys.exit(1)
|
||||
0
app/config/__init__.py
Normal file
0
app/config/__init__.py
Normal file
34
app/config/prompts.py
Normal file
34
app/config/prompts.py
Normal file
@ -0,0 +1,34 @@
|
||||
# app/config/prompts.py
|
||||
"""
|
||||
HealthSync Motivator Batch 프롬프트 설정
|
||||
"""
|
||||
|
||||
class PromptConfig:
|
||||
"""독려 메시지 프롬프트 템플릿"""
|
||||
|
||||
ENCOURAGEMENT_PROMPT = """
|
||||
당신은 건강 미션을 격려하는 따뜻한 AI 코치입니다.
|
||||
|
||||
**독려 메시지 작성 원칙:**
|
||||
- 정확히 1줄로 작성 (50자 내외)
|
||||
- 따뜻하고 격려적인 톤
|
||||
- 적절한 이모지 2-3개 사용
|
||||
- 미션의 구체적인 내용을 반영
|
||||
- 지속적인 동기부여 메시지 포함
|
||||
|
||||
**사용자 정보:**
|
||||
- 직업: {occupation}
|
||||
- 미완료 미션들: {incomplete_missions}
|
||||
|
||||
위 정보를 바탕으로 사용자가 미션을 완료하도록 격려하는 메시지 1줄을 작성해주세요.
|
||||
메시지만 작성하고 다른 말은 하지 마세요.
|
||||
|
||||
예시:
|
||||
💪 오늘도 건강한 하루를 위해 미션을 완료해보세요! 작은 실천이 큰 변화를 만들어요! ✨
|
||||
🌟 바쁜 하루 중에도 잠깐의 시간을 내어 건강을 챙겨보세요! 당신의 몸이 고마워할 거예요! 💚
|
||||
🚶♀️ 오늘의 미션이 기다리고 있어요! 건강한 습관으로 더 나은 내일을 만들어가요! 🌈
|
||||
"""
|
||||
|
||||
def get_encouragement_prompt() -> str:
|
||||
"""독려 메시지 프롬프트 템플릿 반환"""
|
||||
return PromptConfig.ENCOURAGEMENT_PROMPT
|
||||
52
app/config/settings.py
Normal file
52
app/config/settings.py
Normal file
@ -0,0 +1,52 @@
|
||||
# app/config/settings.py
|
||||
"""
|
||||
HealthSync Motivator Batch 환경설정
|
||||
"""
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# .env 파일 로드
|
||||
load_dotenv()
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Motivator Batch 애플리케이션 설정 클래스"""
|
||||
|
||||
# 기본 앱 설정
|
||||
app_name: str = "HealthSync Motivator Batch"
|
||||
app_version: str = "1.0.0"
|
||||
debug: bool = True
|
||||
|
||||
# PostgreSQL 설정
|
||||
db_host: str = "localhost"
|
||||
db_port: int = 5432
|
||||
db_name: str = "healthsync_ai"
|
||||
db_username: str = "postgres"
|
||||
db_password: str = "password"
|
||||
|
||||
# Claude AI 설정
|
||||
claude_api_key: str = ""
|
||||
claude_model: str = "claude-3-5-sonnet-20241022"
|
||||
claude_max_tokens: int = 150
|
||||
claude_temperature: float = 0.7
|
||||
claude_timeout: int = 30
|
||||
|
||||
# 배치 설정
|
||||
batch_size: int = 100
|
||||
max_retries: int = 3
|
||||
|
||||
# 로깅 설정
|
||||
log_level: str = "INFO"
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
"""데이터베이스 URL 생성"""
|
||||
return f"postgresql://{self.db_username}:{self.db_password}@{self.db_host}:{self.db_port}/{self.db_name}"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = False
|
||||
|
||||
# 전역 설정 인스턴스
|
||||
settings = Settings()
|
||||
9
app/models/__init__.py
Normal file
9
app/models/__init__.py
Normal file
@ -0,0 +1,9 @@
|
||||
# app/models/__init__.py
|
||||
"""
|
||||
HealthSync Motivator Batch 모델 패키지
|
||||
"""
|
||||
from .user import User
|
||||
from .goal import UserMissionGoal, MissionCompletionHistory
|
||||
from .chat_message import ChatMessage
|
||||
|
||||
__all__ = ["User", "UserMissionGoal", "MissionCompletionHistory", "ChatMessage"]
|
||||
28
app/models/chat_message.py
Normal file
28
app/models/chat_message.py
Normal file
@ -0,0 +1,28 @@
|
||||
# app/models/chat_message.py
|
||||
"""
|
||||
HealthSync AI 채팅 메시지 모델
|
||||
"""
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
class MessageType(str, Enum):
|
||||
"""메시지 타입"""
|
||||
CONSULTATION = "consultation" # 상담
|
||||
CELEBRATION = "celebration" # 축하
|
||||
ENCOURAGEMENT = "encouragement" # 독려
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
"""채팅 메시지 모델"""
|
||||
message_id: Optional[int] = Field(None, description="메시지 ID")
|
||||
member_serial_number: int = Field(..., description="회원 일련번호")
|
||||
message_type: str = Field(..., max_length=20, description="메시지 타입")
|
||||
message_content: Optional[str] = Field(None, description="메시지 내용")
|
||||
response_content: Optional[str] = Field(None, description="AI 응답 내용")
|
||||
created_at: datetime = Field(default_factory=datetime.now, description="생성일시")
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat()
|
||||
}
|
||||
40
app/models/goal.py
Normal file
40
app/models/goal.py
Normal file
@ -0,0 +1,40 @@
|
||||
# app/models/goal.py
|
||||
"""
|
||||
HealthSync Motivator Batch 목표 관련 모델 (goal_service)
|
||||
"""
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime, date
|
||||
from typing import Optional
|
||||
|
||||
class UserMissionGoal(BaseModel):
|
||||
"""사용자 미션 목표 (goal_service.user_mission_goal 테이블)"""
|
||||
mission_id: int = Field(..., description="미션 ID")
|
||||
member_serial_number: int = Field(..., description="회원 일련번호")
|
||||
performance_date: date = Field(..., description="수행 날짜")
|
||||
mission_name: str = Field(..., max_length=100, description="미션명")
|
||||
mission_description: Optional[str] = Field(None, max_length=200, description="미션 설명")
|
||||
daily_target_count: int = Field(..., description="일일 목표 횟수")
|
||||
is_active: bool = Field(..., description="활성 상태")
|
||||
created_at: datetime = Field(..., description="생성일시")
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat(),
|
||||
date: lambda v: v.isoformat()
|
||||
}
|
||||
|
||||
class MissionCompletionHistory(BaseModel):
|
||||
"""미션 완료 이력 (goal_service.mission_completion_history 테이블)"""
|
||||
completion_id: int = Field(..., description="완료 ID")
|
||||
mission_id: int = Field(..., description="미션 ID")
|
||||
member_serial_number: int = Field(..., description="회원 일련번호")
|
||||
completion_date: date = Field(..., description="완료 날짜")
|
||||
daily_target_count: int = Field(..., description="일일 목표 횟수")
|
||||
daily_completed_count: int = Field(..., description="일일 완료 횟수")
|
||||
created_at: datetime = Field(..., description="생성일시")
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat(),
|
||||
date: lambda v: v.isoformat()
|
||||
}
|
||||
24
app/models/user.py
Normal file
24
app/models/user.py
Normal file
@ -0,0 +1,24 @@
|
||||
# app/models/user.py
|
||||
"""
|
||||
HealthSync Motivator Batch 사용자 모델 (user_service.user)
|
||||
"""
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime, date
|
||||
from typing import Optional
|
||||
|
||||
class User(BaseModel):
|
||||
"""사용자 모델 (user_service.user 테이블)"""
|
||||
member_serial_number: int = Field(..., description="회원 일련번호")
|
||||
google_id: str = Field(..., max_length=255, description="구글 ID")
|
||||
name: str = Field(..., max_length=100, description="사용자 이름")
|
||||
birth_date: date = Field(..., description="생년월일")
|
||||
occupation: Optional[str] = Field(None, max_length=50, description="직업")
|
||||
created_at: datetime = Field(..., description="생성일시")
|
||||
updated_at: datetime = Field(..., description="수정일시")
|
||||
last_login_at: Optional[datetime] = Field(None, description="마지막 로그인")
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat(),
|
||||
date: lambda v: v.isoformat()
|
||||
}
|
||||
4
app/repositories/__init__.py
Normal file
4
app/repositories/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# app/repositories/__init__.py
|
||||
"""
|
||||
HealthSync Motivator Batch 리포지토리 패키지
|
||||
"""
|
||||
10
app/repositories/queries/__init__.py
Normal file
10
app/repositories/queries/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
# app/repositories/queries/__init__.py
|
||||
"""
|
||||
HealthSync Motivator Batch 쿼리 패키지
|
||||
"""
|
||||
from .base_queries import BaseQueries
|
||||
from .user_queries import UserQueries
|
||||
from .goal_queries import GoalQueries
|
||||
from .chat_queries import ChatQueries
|
||||
|
||||
__all__ = ["BaseQueries", "UserQueries", "GoalQueries", "ChatQueries"]
|
||||
13
app/repositories/queries/base_queries.py
Normal file
13
app/repositories/queries/base_queries.py
Normal file
@ -0,0 +1,13 @@
|
||||
# app/repositories/queries/base_queries.py
|
||||
"""
|
||||
HealthSync Motivator Batch 기본 쿼리
|
||||
"""
|
||||
|
||||
class BaseQueries:
|
||||
"""기본 시스템 쿼리"""
|
||||
|
||||
# 데이터베이스 연결 테스트
|
||||
CONNECTION_TEST = "SELECT 1"
|
||||
|
||||
# 현재 시간 조회
|
||||
CURRENT_TIMESTAMP = "SELECT NOW()"
|
||||
23
app/repositories/queries/chat_queries.py
Normal file
23
app/repositories/queries/chat_queries.py
Normal file
@ -0,0 +1,23 @@
|
||||
# app/repositories/queries/chat_queries.py
|
||||
"""
|
||||
HealthSync Motivator Batch 채팅 메시지 관련 쿼리
|
||||
"""
|
||||
|
||||
class ChatQueries:
|
||||
"""채팅 메시지 관련 쿼리"""
|
||||
|
||||
# 독려 메시지 저장 (수정된 쿼리)
|
||||
INSERT_ENCOURAGEMENT_MESSAGE = """
|
||||
INSERT INTO intelligence_service.chat_message
|
||||
(member_serial_number, message_type, message_content, response_content, created_at)
|
||||
VALUES (:member_serial_number, :message_type, :message_content, :response_content, :created_at)
|
||||
"""
|
||||
|
||||
# 오늘 이미 독려 메시지를 받았는지 확인
|
||||
CHECK_TODAY_ENCOURAGEMENT = """
|
||||
SELECT COUNT(*) as count
|
||||
FROM intelligence_service.chat_message cm
|
||||
WHERE cm.member_serial_number = :user_id
|
||||
AND cm.message_type = 'encouragement'
|
||||
AND DATE(cm.created_at) = CURRENT_DATE
|
||||
"""
|
||||
54
app/repositories/queries/goal_queries.py
Normal file
54
app/repositories/queries/goal_queries.py
Normal file
@ -0,0 +1,54 @@
|
||||
# app/repositories/queries/goal_queries.py
|
||||
"""
|
||||
HealthSync Motivator Batch 목표/미션 관련 쿼리
|
||||
"""
|
||||
|
||||
class GoalQueries:
|
||||
"""목표/미션 관련 쿼리"""
|
||||
|
||||
# 사용자별 활성 미션 조회
|
||||
GET_ACTIVE_MISSIONS_BY_USER = """
|
||||
SELECT
|
||||
umg.mission_id,
|
||||
umg.member_serial_number,
|
||||
umg.mission_name,
|
||||
umg.mission_description,
|
||||
umg.daily_target_count,
|
||||
umg.performance_date
|
||||
FROM goal_service.user_mission_goal umg
|
||||
WHERE umg.member_serial_number = :user_id
|
||||
AND umg.is_active = true
|
||||
AND umg.performance_date = CURRENT_DATE
|
||||
"""
|
||||
|
||||
# 오늘 완료된 미션 조회
|
||||
GET_TODAY_COMPLETED_MISSIONS = """
|
||||
SELECT
|
||||
mch.mission_id,
|
||||
mch.member_serial_number,
|
||||
mch.daily_target_count,
|
||||
mch.daily_completed_count
|
||||
FROM goal_service.mission_completion_history mch
|
||||
WHERE mch.member_serial_number = :user_id
|
||||
AND mch.completion_date = CURRENT_DATE
|
||||
AND mch.daily_completed_count >= mch.daily_target_count
|
||||
"""
|
||||
|
||||
# 미완료 미션 조회 (오늘 활성 미션 중 완료되지 않은 것들)
|
||||
GET_INCOMPLETE_MISSIONS = """
|
||||
SELECT
|
||||
umg.mission_id,
|
||||
umg.mission_name,
|
||||
umg.mission_description,
|
||||
mch.daily_completed_count,
|
||||
umg.daily_target_count,
|
||||
mch.completion_date
|
||||
FROM goal_service.user_mission_goal umg
|
||||
LEFT JOIN goal_service.mission_completion_history mch
|
||||
ON umg.mission_id = mch.mission_id
|
||||
AND umg.member_serial_number = mch.member_serial_number
|
||||
AND mch.completion_date = CURRENT_DATE
|
||||
WHERE umg.member_serial_number = :user_id
|
||||
AND ((mch.completion_date = CURRENT_DATE AND mch.daily_completed_count < mch.daily_target_count)
|
||||
OR mch.completion_date IS NULL);
|
||||
"""
|
||||
28
app/repositories/queries/user_queries.py
Normal file
28
app/repositories/queries/user_queries.py
Normal file
@ -0,0 +1,28 @@
|
||||
# app/repositories/queries/user_queries.py
|
||||
"""
|
||||
HealthSync Motivator Batch 사용자 관련 쿼리
|
||||
"""
|
||||
|
||||
class UserQueries:
|
||||
"""사용자 관련 쿼리"""
|
||||
|
||||
# 활성 사용자 목록 조회
|
||||
GET_ACTIVE_USERS = """
|
||||
SELECT
|
||||
u.member_serial_number,
|
||||
u.name,
|
||||
u.occupation
|
||||
FROM user_service.user u
|
||||
WHERE u.last_login_at >= NOW() - INTERVAL '30 days'
|
||||
ORDER BY u.member_serial_number
|
||||
"""
|
||||
|
||||
# 특정 사용자 정보 조회
|
||||
GET_USER_BY_ID = """
|
||||
SELECT
|
||||
u.member_serial_number,
|
||||
u.name,
|
||||
u.occupation
|
||||
FROM user_service.user u
|
||||
WHERE u.member_serial_number = :user_id
|
||||
"""
|
||||
4
app/services/__init__.py
Normal file
4
app/services/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# app/services/__init__.py
|
||||
"""
|
||||
HealthSync Motivator Batch 서비스 패키지
|
||||
"""
|
||||
165
app/services/motivation_service.py
Normal file
165
app/services/motivation_service.py
Normal file
@ -0,0 +1,165 @@
|
||||
# app/services/motivation_service.py
|
||||
"""
|
||||
HealthSync Motivator Batch 독려 메시지 생성 서비스
|
||||
"""
|
||||
import asyncio
|
||||
from typing import Dict, Any, List
|
||||
from datetime import datetime
|
||||
from app.utils.database_utils import db
|
||||
from app.utils.claude_client import claude_client
|
||||
from app.utils.logger import batch_logger
|
||||
from app.config.prompts import get_encouragement_prompt
|
||||
from app.repositories.queries import UserQueries, GoalQueries, ChatQueries
|
||||
from app.models.chat_message import MessageType
|
||||
|
||||
|
||||
class MotivationService:
|
||||
"""독려 메시지 생성 비즈니스 로직 서비스"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = batch_logger
|
||||
|
||||
async def process_all_users(self) -> Dict[str, int]:
|
||||
"""모든 활성 사용자에 대해 독려 메시지 처리"""
|
||||
try:
|
||||
self.logger.info("🚀 독려 메시지 배치 처리 시작")
|
||||
|
||||
# 1. 활성 사용자 목록 조회
|
||||
active_users = await db.execute_query(UserQueries.GET_ACTIVE_USERS)
|
||||
self.logger.info(f"📊 활성 사용자 수: {len(active_users)}명")
|
||||
|
||||
# 처리 결과 카운터
|
||||
results = {
|
||||
"total_users": len(active_users),
|
||||
"processed_users": 0,
|
||||
"sent_messages": 0,
|
||||
"skipped_users": 0,
|
||||
"failed_users": 0
|
||||
}
|
||||
|
||||
# 2. 각 사용자별 독려 메시지 처리
|
||||
for user in active_users:
|
||||
try:
|
||||
user_id = user["member_serial_number"]
|
||||
self.logger.info(f"👤 사용자 처리 시작: {user['name']} (ID: {user_id})")
|
||||
|
||||
# 사용자별 독려 메시지 처리
|
||||
message_sent = await self._process_user_encouragement(user)
|
||||
|
||||
results["processed_users"] += 1
|
||||
if message_sent:
|
||||
results["sent_messages"] += 1
|
||||
else:
|
||||
results["skipped_users"] += 1
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"❌ 사용자 처리 실패 - user_id: {user.get('member_serial_number')}, error: {str(e)}")
|
||||
results["failed_users"] += 1
|
||||
|
||||
self.logger.info(f"✅ 독려 메시지 배치 처리 완료: {results}")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"❌ 배치 처리 전체 실패: {str(e)}")
|
||||
raise Exception(f"배치 처리 실패: {str(e)}")
|
||||
|
||||
async def _process_user_encouragement(self, user: Dict[str, Any]) -> bool:
|
||||
"""개별 사용자 독려 메시지 처리"""
|
||||
try:
|
||||
user_id = user["member_serial_number"]
|
||||
user_name = user["name"]
|
||||
occupation = user.get("occupation", "정보 없음")
|
||||
|
||||
# # 1. 오늘 이미 독려 메시지를 받았는지 확인
|
||||
# today_encouragement = await db.execute_query(
|
||||
# ChatQueries.CHECK_TODAY_ENCOURAGEMENT,
|
||||
# {"user_id": user_id}
|
||||
# )
|
||||
#
|
||||
# if today_encouragement and today_encouragement[0]["count"] > 0:
|
||||
# self.logger.info(f"⏭️ 이미 독려 메시지 발송됨 - user: {user_name}")
|
||||
# return False
|
||||
|
||||
# 2. 미완료 미션 조회
|
||||
incomplete_missions = await db.execute_query(
|
||||
GoalQueries.GET_INCOMPLETE_MISSIONS,
|
||||
{"user_id": user_id}
|
||||
)
|
||||
|
||||
if not incomplete_missions:
|
||||
self.logger.info(f"✅ 미완료 미션 없음 - user: {user_name}")
|
||||
return False
|
||||
|
||||
# 3. 독려 메시지 생성
|
||||
encouragement_message = await self._generate_encouragement_message(
|
||||
occupation, incomplete_missions
|
||||
)
|
||||
|
||||
# 4. 채팅 DB에 저장
|
||||
await self._save_encouragement_message(user_id, encouragement_message)
|
||||
|
||||
self.logger.info(f"💌 독려 메시지 발송 완료 - user: {user_name}, "
|
||||
f"미완료 미션: {len(incomplete_missions)}개")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"❌ 사용자 독려 메시지 처리 실패 - user_id: {user_id}, error: {str(e)}")
|
||||
raise Exception(f"사용자 독려 메시지 처리 실패: {str(e)}")
|
||||
|
||||
async def _generate_encouragement_message(self, occupation: str, incomplete_missions: List[Dict[str, Any]]) -> str:
|
||||
"""Claude API를 통한 독려 메시지 생성"""
|
||||
try:
|
||||
# 미완료 미션 목록 문자열 생성
|
||||
mission_list = []
|
||||
for mission in incomplete_missions:
|
||||
mission_list.append(f"- {mission['mission_name']} (목표: {mission['daily_target_count']}회)")
|
||||
|
||||
missions_text = "\n".join(mission_list)
|
||||
|
||||
# 프롬프트 생성
|
||||
prompt_template = get_encouragement_prompt()
|
||||
formatted_prompt = prompt_template.format(
|
||||
occupation=occupation,
|
||||
incomplete_missions=missions_text
|
||||
)
|
||||
|
||||
# Claude API 호출
|
||||
claude_response = await claude_client.call_claude_api(formatted_prompt)
|
||||
|
||||
# 응답 정제 (앞뒤 공백 제거, 따옴표 제거)
|
||||
encouragement_message = claude_response.strip().strip('"').strip("'")
|
||||
|
||||
self.logger.info(f"💭 독려 메시지 생성 완료 - 길이: {len(encouragement_message)}자")
|
||||
return encouragement_message
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"❌ 독려 메시지 생성 실패: {str(e)}")
|
||||
raise Exception(f"독려 메시지 생성 실패: {str(e)}")
|
||||
|
||||
async def _save_encouragement_message(self, user_id: int, message: str) -> None:
|
||||
"""독려 메시지를 채팅 DB에 저장 (수정된 메서드)"""
|
||||
try:
|
||||
# ChatMessage 모델에 맞춰 데이터 구성
|
||||
message_data = {
|
||||
"member_serial_number": user_id,
|
||||
"message_type": MessageType.ENCOURAGEMENT.value, # "encouragement"
|
||||
"message_content": None, # 사용자 입력이 없으므로 None
|
||||
"response_content": message, # AI가 생성한 독려 메시지
|
||||
"created_at": datetime.now()
|
||||
}
|
||||
|
||||
await db.execute_insert(
|
||||
ChatQueries.INSERT_ENCOURAGEMENT_MESSAGE,
|
||||
message_data
|
||||
)
|
||||
|
||||
self.logger.info(f"💾 독려 메시지 DB 저장 완료 - user_id: {user_id}, "
|
||||
f"message_type: {MessageType.ENCOURAGEMENT.value}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"❌ 독려 메시지 DB 저장 실패 - user_id: {user_id}, error: {str(e)}")
|
||||
raise Exception(f"독려 메시지 DB 저장 실패: {str(e)}")
|
||||
|
||||
|
||||
# 전역 서비스 인스턴스
|
||||
motivation_service = MotivationService()
|
||||
4
app/utils/__init__.py
Normal file
4
app/utils/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# app/utils/__init__.py
|
||||
"""
|
||||
HealthSync Motivator Batch 유틸리티 패키지
|
||||
"""
|
||||
72
app/utils/claude_client.py
Normal file
72
app/utils/claude_client.py
Normal file
@ -0,0 +1,72 @@
|
||||
# app/utils/claude_client.py
|
||||
"""
|
||||
HealthSync Motivator Batch Claude API 클라이언트
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
import anthropic
|
||||
from app.config.settings import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ClaudeClient:
|
||||
"""Claude API 호출 클라이언트"""
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = settings.claude_api_key
|
||||
|
||||
# API 키 검증
|
||||
if not self.api_key or self.api_key == "" or self.api_key == "your_claude_api_key_here":
|
||||
raise ValueError("Claude API 키가 설정되지 않았습니다. .env 파일에 CLAUDE_API_KEY를 설정해주세요.")
|
||||
|
||||
# 동기 클라이언트 초기화
|
||||
self.client = anthropic.Anthropic(api_key=self.api_key)
|
||||
logger.info(f"✅ Claude API 클라이언트 초기화 완료")
|
||||
|
||||
async def call_claude_api(self, prompt: str) -> str:
|
||||
"""Claude API 호출 (비동기 래퍼)"""
|
||||
try:
|
||||
logger.info(f"🚀 Claude API 호출 시작 (모델: {settings.claude_model})")
|
||||
|
||||
# 동기 함수를 비동기로 실행
|
||||
def sync_call():
|
||||
return self.client.messages.create(
|
||||
model=settings.claude_model,
|
||||
max_tokens=settings.claude_max_tokens,
|
||||
temperature=settings.claude_temperature,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
# 스레드 풀에서 동기 함수 실행
|
||||
message = await asyncio.get_event_loop().run_in_executor(None, sync_call)
|
||||
|
||||
logger.info("✅ Claude API 호출 성공")
|
||||
return message.content[0].text
|
||||
|
||||
except anthropic.AuthenticationError as e:
|
||||
logger.error("❌ Claude API 인증 실패 - API 키 확인 필요")
|
||||
raise Exception(f"Claude API 인증 실패: {str(e)}")
|
||||
|
||||
except anthropic.NotFoundError as e:
|
||||
logger.error("❌ Claude API 엔드포인트 또는 모델을 찾을 수 없음")
|
||||
raise Exception(f"Claude API 모델 또는 엔드포인트 오류: {str(e)}")
|
||||
|
||||
except anthropic.RateLimitError as e:
|
||||
logger.error("❌ Claude API 요청 한도 초과")
|
||||
raise Exception(f"Claude API 요청 한도 초과: {str(e)}")
|
||||
|
||||
except anthropic.APITimeoutError as e:
|
||||
logger.error("⏰ Claude API 타임아웃")
|
||||
raise Exception(f"Claude API 타임아웃: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Claude API 호출 중 예상치 못한 오류: {str(e)}")
|
||||
raise Exception(f"Claude API 호출 실패: {str(e)}")
|
||||
|
||||
# 전역 클라이언트 인스턴스
|
||||
claude_client = ClaudeClient()
|
||||
86
app/utils/database_utils.py
Normal file
86
app/utils/database_utils.py
Normal file
@ -0,0 +1,86 @@
|
||||
# app/utils/database_utils.py
|
||||
"""
|
||||
HealthSync Motivator Batch 데이터베이스 유틸리티
|
||||
"""
|
||||
import databases
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
from app.config.settings import settings
|
||||
from app.repositories.queries import BaseQueries
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SimpleDatabase:
|
||||
"""PostgreSQL 데이터베이스 연결 클래스"""
|
||||
|
||||
def __init__(self):
|
||||
self.database = databases.Database(settings.database_url)
|
||||
self._connected = False
|
||||
|
||||
async def connect(self):
|
||||
"""데이터베이스 연결"""
|
||||
if not self._connected:
|
||||
try:
|
||||
await self.database.connect()
|
||||
self._connected = True
|
||||
logger.info("데이터베이스 연결 성공")
|
||||
except Exception as e:
|
||||
logger.error(f"데이터베이스 연결 실패: {str(e)}")
|
||||
raise
|
||||
|
||||
async def disconnect(self):
|
||||
"""데이터베이스 연결 해제"""
|
||||
if self._connected:
|
||||
try:
|
||||
await self.database.disconnect()
|
||||
self._connected = False
|
||||
logger.info("데이터베이스 연결 해제")
|
||||
except Exception as e:
|
||||
logger.error(f"데이터베이스 연결 해제 실패: {str(e)}")
|
||||
|
||||
async def test_connection(self) -> bool:
|
||||
"""데이터베이스 연결 테스트"""
|
||||
try:
|
||||
if not self._connected:
|
||||
await self.connect()
|
||||
|
||||
result = await self.database.fetch_val(BaseQueries.CONNECTION_TEST)
|
||||
return result == 1
|
||||
except Exception as e:
|
||||
logger.error(f"데이터베이스 연결 테스트 실패: {str(e)}")
|
||||
return False
|
||||
|
||||
async def execute_query(self, query: str, values: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
|
||||
"""쿼리 실행 (SELECT)"""
|
||||
try:
|
||||
if not self._connected:
|
||||
await self.connect()
|
||||
|
||||
rows = await self.database.fetch_all(query, values or {})
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"쿼리 실행 실패: {str(e)}")
|
||||
logger.error(f"쿼리: {query}")
|
||||
logger.error(f"파라미터: {values}")
|
||||
raise Exception(f"쿼리 실행 실패: {str(e)}")
|
||||
|
||||
async def execute_insert(self, query: str, values: Optional[Dict[str, Any]] = None) -> int:
|
||||
"""INSERT 쿼리 실행"""
|
||||
try:
|
||||
if not self._connected:
|
||||
await self.connect()
|
||||
|
||||
result = await self.database.execute(query, values or {})
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"INSERT 실행 실패: {str(e)}")
|
||||
logger.error(f"쿼리: {query}")
|
||||
logger.error(f"파라미터: {values}")
|
||||
raise Exception(f"INSERT 실행 실패: {str(e)}")
|
||||
|
||||
|
||||
# 전역 데이터베이스 인스턴스
|
||||
db = SimpleDatabase()
|
||||
37
app/utils/logger.py
Normal file
37
app/utils/logger.py
Normal file
@ -0,0 +1,37 @@
|
||||
# app/utils/logger.py
|
||||
"""
|
||||
HealthSync Motivator Batch 로깅 설정
|
||||
"""
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from app.config.settings import settings
|
||||
|
||||
|
||||
def setup_logger(name: str = "motivator_batch") -> logging.Logger:
|
||||
"""로거 설정"""
|
||||
logger = logging.getLogger(name)
|
||||
|
||||
# 이미 핸들러가 있으면 중복 추가 방지
|
||||
if logger.handlers:
|
||||
return logger
|
||||
|
||||
logger.setLevel(getattr(logging, settings.log_level.upper()))
|
||||
|
||||
# 콘솔 핸들러
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setLevel(getattr(logging, settings.log_level.upper()))
|
||||
|
||||
# 포맷터
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
logger.addHandler(console_handler)
|
||||
return logger
|
||||
|
||||
|
||||
# 전역 로거
|
||||
batch_logger = setup_logger()
|
||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@ -0,0 +1,8 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
pydantic>=2.8.0
|
||||
pydantic-settings>=2.4.0
|
||||
python-dotenv==1.0.0
|
||||
databases[postgresql]==0.8.0
|
||||
anthropic>=0.40.0
|
||||
asyncio-mqtt==0.13.0
|
||||
Loading…
x
Reference in New Issue
Block a user