203 lines
9.1 KiB
Python
203 lines
9.1 KiB
Python
# app/services/chat_service.py
|
|
"""
|
|
HealthSync AI 챗봇 상담 서비스
|
|
"""
|
|
from typing import Dict, Any, List
|
|
from datetime import datetime
|
|
from app.services.base_service import BaseService
|
|
from app.services.health_service import HealthService
|
|
from app.utils.claude_client import ClaudeClient
|
|
from app.config.prompts import get_chat_consultation_prompt
|
|
from app.dto.response.chat_response import ChatResponse
|
|
from app.dto.response.chat_history_response import ChatHistoryResponse, ChatHistoryItem
|
|
from app.repositories.chat_repository import ChatRepository
|
|
|
|
|
|
class ChatService(BaseService):
|
|
"""챗봇 상담 비즈니스 로직 서비스"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.health_service = HealthService()
|
|
self.claude_client = ClaudeClient()
|
|
self.chat_repository = ChatRepository()
|
|
|
|
async def get_health_consultation(self, user_id: int, message: str) -> ChatResponse:
|
|
"""건강 상담 챗봇 응답 생성 및 저장 (consultation 타입 단일 레코드 업데이트 방식)"""
|
|
consultation_message_id = None
|
|
try:
|
|
self.log_operation("get_health_consultation_start", user_id=user_id,
|
|
message_length=len(message))
|
|
|
|
# 1. consultation 타입으로 질문과 "응답 생성중" 메시지를 한 번에 저장
|
|
consultation_message_id = await self.chat_repository.save_chat_message(
|
|
user_id=user_id,
|
|
message_type="consultation",
|
|
message_content=message,
|
|
response_content="💭 응답을 생성하고 있습니다..."
|
|
)
|
|
|
|
# 2. 사용자 건강 데이터 조회
|
|
combined_data = await self.health_service.get_combined_user_health_data(user_id)
|
|
user_data = combined_data["user_info"]
|
|
health_data = combined_data["health_data"]
|
|
|
|
# 3. 상담 프롬프트 생성
|
|
prompt = await self._build_consultation_prompt(user_data, health_data, message)
|
|
|
|
# 4. Claude API 호출
|
|
claude_response = await self.claude_client.call_claude_api(prompt)
|
|
|
|
# 5. 응답 정제
|
|
ai_response = claude_response.strip()
|
|
|
|
# 6. "응답 생성중" 메시지를 실제 응답으로 업데이트
|
|
await self.chat_repository.update_chat_message_response(
|
|
message_id=consultation_message_id,
|
|
response_content=ai_response
|
|
)
|
|
|
|
# 7. 최종 응답 생성
|
|
response = ChatResponse(
|
|
response=ai_response,
|
|
timestamp=datetime.now()
|
|
)
|
|
|
|
self.log_operation("get_health_consultation_success", user_id=user_id,
|
|
consultation_id=consultation_message_id,
|
|
response_length=len(ai_response))
|
|
|
|
return response
|
|
|
|
except Exception as e:
|
|
# 오류 발생 시 "응답 생성중" 메시지를 오류 메시지로 업데이트
|
|
if consultation_message_id:
|
|
try:
|
|
await self.chat_repository.update_chat_message_response(
|
|
message_id=consultation_message_id,
|
|
response_content="❌ 죄송합니다. 응답 생성 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
|
|
)
|
|
except:
|
|
pass # 업데이트 실패 시에도 원본 오류를 유지
|
|
|
|
self.logger.error(f"건강 상담 처리 실패 - user_id: {user_id}, error: {str(e)}")
|
|
raise Exception(f"건강 상담 처리 실패: {str(e)}")
|
|
|
|
async def get_chat_history(self, user_id: int) -> ChatHistoryResponse:
|
|
"""사용자 채팅 이력 조회 (NULL 값 그대로 반환)"""
|
|
try:
|
|
self.log_operation("get_chat_history_start", user_id=user_id)
|
|
|
|
# 1. 데이터베이스에서 채팅 이력 조회
|
|
chat_records = await self.chat_repository.get_chat_history_by_user_id(user_id)
|
|
|
|
# 2. DTO로 변환 (NULL 값 처리)
|
|
chat_history_items = []
|
|
for record in chat_records:
|
|
chat_item = ChatHistoryItem(
|
|
message_id=record["message_id"],
|
|
message_type=record["message_type"],
|
|
message_content=record.get("message_content"),
|
|
response_content=record["response_content"] or "",
|
|
created_at=record["created_at"]
|
|
)
|
|
chat_history_items.append(chat_item)
|
|
|
|
# 3. 응답 생성
|
|
response = ChatHistoryResponse(
|
|
chat_history=chat_history_items,
|
|
total_count=len(chat_history_items)
|
|
)
|
|
|
|
self.log_operation("get_chat_history_success", user_id=user_id,
|
|
total_count=len(chat_history_items),
|
|
null_message_count=sum(1 for item in chat_history_items if item.message_content is None))
|
|
|
|
return response
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"채팅 이력 조회 실패 - user_id: {user_id}, error: {str(e)}")
|
|
raise Exception(f"채팅 이력 조회 실패: {str(e)}")
|
|
|
|
async def _build_consultation_prompt(self, user_data: Dict[str, Any],
|
|
health_data: Dict[str, Any], message: str) -> str:
|
|
"""사용자 데이터와 질문을 기반으로 상담 프롬프트 생성"""
|
|
try:
|
|
prompt_template = get_chat_consultation_prompt()
|
|
|
|
# BMI 계산
|
|
bmi = health_data.get("bmi", 0)
|
|
if not bmi and health_data.get("height") and health_data.get("weight"):
|
|
height_m = health_data["height"] / 100
|
|
bmi = round(health_data["weight"] / (height_m ** 2), 1)
|
|
|
|
# 프롬프트에 데이터 매핑
|
|
formatted_prompt = prompt_template.format(
|
|
# 사용자 질문
|
|
user_question=message,
|
|
|
|
# 사용자 기본 정보
|
|
occupation=user_data.get("occupation", "정보 없음"),
|
|
name=user_data.get("name", "정보 없음"),
|
|
age=user_data.get("age", health_data.get("age", "정보 없음")),
|
|
|
|
# 신체 정보
|
|
height=health_data.get("height", "정보 없음"),
|
|
weight=health_data.get("weight", "정보 없음"),
|
|
bmi=bmi if bmi else "정보 없음",
|
|
waist_circumference=health_data.get("waist_circumference", "정보 없음"),
|
|
|
|
# 혈압 및 혈당
|
|
systolic_bp=health_data.get("systolic_bp", "정보 없음"),
|
|
diastolic_bp=health_data.get("diastolic_bp", "정보 없음"),
|
|
fasting_glucose=health_data.get("fasting_glucose", "정보 없음"),
|
|
|
|
# 콜레스테롤
|
|
total_cholesterol=health_data.get("total_cholesterol", "정보 없음"),
|
|
hdl_cholesterol=health_data.get("hdl_cholesterol", "정보 없음"),
|
|
ldl_cholesterol=health_data.get("ldl_cholesterol", "정보 없음"),
|
|
triglyceride=health_data.get("triglyceride", "정보 없음"),
|
|
|
|
# 기타 혈액 검사
|
|
hemoglobin=health_data.get("hemoglobin", "정보 없음"),
|
|
serum_creatinine=health_data.get("serum_creatinine", "정보 없음"),
|
|
ast=health_data.get("ast", "정보 없음"),
|
|
alt=health_data.get("alt", "정보 없음"),
|
|
gamma_gtp=health_data.get("gamma_gtp", "정보 없음"),
|
|
urine_protein=health_data.get("urine_protein", "정보 없음"),
|
|
|
|
# 감각기관
|
|
visual_acuity_left=health_data.get("visual_acuity_left", "정보 없음"),
|
|
visual_acuity_right=health_data.get("visual_acuity_right", "정보 없음"),
|
|
hearing_left=health_data.get("hearing_left", "정보 없음"),
|
|
hearing_right=health_data.get("hearing_right", "정보 없음"),
|
|
|
|
# 생활습관
|
|
smoking_status=self._convert_smoking_status(health_data.get("smoking_status")),
|
|
drinking_status=self._convert_drinking_status(health_data.get("drinking_status"))
|
|
)
|
|
|
|
self.log_operation("build_consultation_prompt", user_id="N/A",
|
|
prompt_length=len(formatted_prompt))
|
|
return formatted_prompt
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"상담 프롬프트 생성 실패: {str(e)}")
|
|
raise Exception(f"상담 프롬프트 생성 실패: {str(e)}")
|
|
|
|
def _convert_smoking_status(self, status: int) -> str:
|
|
"""흡연 상태 코드를 텍스트로 변환"""
|
|
smoking_map = {
|
|
0: "비흡연",
|
|
1: "과거 흡연",
|
|
2: "현재 흡연"
|
|
}
|
|
return smoking_map.get(status, "정보 없음")
|
|
|
|
def _convert_drinking_status(self, status: int) -> str:
|
|
"""음주 상태 코드를 텍스트로 변환"""
|
|
drinking_map = {
|
|
0: "비음주",
|
|
1: "음주"
|
|
}
|
|
return drinking_map.get(status, "정보 없음") |