hyerimmy 910bd902b1
Some checks failed
HealthSync Intelligence CI / build-and-push (push) Has been cancelled
feat : initial commit
2025-06-20 05:28:30 +00:00

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, "정보 없음")