Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions chatbot/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ def __init__(self):
self.anthropic_api_key = os.environ.get('ANTHROPIC_API_KEY')
self.gcp_ssm_param_name = os.environ.get('GCP_SSM_PARAM_NAME')

# Gemma 3 설정 (Hugging Face)
self.hf_endpoint_url = os.environ.get("HF_ENDPOINT_URL", "")
self.hf_api_token = os.environ.get("HF_API_TOKEN", "")

# SQS 설정
self.cbt_log_sqs_url = os.environ.get('CBT_LOG_SQS_URL')
self.diary_to_chatbot_sqs_url = os.environ.get('DIARY_TO_CHATBOT_SQS_URL')
Expand Down
118 changes: 118 additions & 0 deletions chatbot/controller/dev_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# chatbot/controller/dev_controller.py
import json
import boto3
import logging
from datetime import datetime
from fastapi import APIRouter, Depends
from exception import AppError
from config import config
from schema.test import MindDiaryTestRequest
from schema.reframing import ReframingRequest, ReframingResponse
from service.llm_service import LLMService, get_llm_service
from prompts.reframing import REFRAMING_PROMPT_TEMPLATE

logger = logging.getLogger()
router = APIRouter(tags=["Dev / Experiment"])

@router.post(
"/chatbot/dev/sqs/mind-diary",
summary="[개발용] 마음일기 분석 완료 이벤트 SQS 전송 테스트",
description="""
마음일기 서비스(caring-back)에서 분석이 완료된 상황을 시뮬레이션하여 SQS 메시지를 강제로 전송합니다.
"""
)
def trigger_mind_diary_event(request: MindDiaryTestRequest):
# 1. 전송할 SQS URL 확인
sqs_url = config.diary_to_chatbot_sqs_url
if not sqs_url:
sqs_url = config.cbt_log_sqs_url # Fallback

if not sqs_url:
raise AppError(
status_code=500,
message="SQS URL이 설정되지 않았습니다."
)

try:
# 2. SQS 메시지 본문 구성
message_body = {
"source": "mind-diary",
"event": "analysis_completed",
"user_id": request.user_id,
"voice_id": 999999, # 테스트용 더미 ID
"user_name": request.user_name,
"question": request.question,
"content": request.content,
"recorded_at": request.recorded_at or datetime.now().isoformat(),
"timestamp": datetime.now().isoformat(),
"emotion": request.emotion.model_dump()
}

# 3. SQS 전송
sqs_client = boto3.client('sqs', region_name='ap-northeast-2')
response = sqs_client.send_message(
QueueUrl=sqs_url,
MessageBody=json.dumps(message_body, ensure_ascii=False)
)

return {
"success": True,
"message": "Simulated Mind Diary event sent to SQS",
"message_id": response.get("MessageId"),
"payload_preview": message_body
}

except Exception as e:
raise AppError(
status_code=500,
message="SQS 메시지 전송에 실패했습니다.",
detail=str(e)
)

@router.post(
"/chatbot/dev/reframing",
response_model=ReframingResponse,
summary="[DEV] Gemma 2 리프레이밍 실험",
description="""
운영 중인 `/chatbot/reframing` API와 **동일한 입력(Request)과 출력(Response)** 규격을 가집니다.
내부적으로 DB를 조회하지 않고, **Gemma 2 (Hugging Face)** 모델을 호출하여 답변을 생성합니다.

- **목적**: 기존 모델(Claude/Gemini)과 Gemma 3의 상담 성능 직접 비교 (A/B 테스트)
- **입력**: ReframingRequest (`user_input` 필수)
- **출력**: ReframingResponse (공감, 분석, 질문, 대안 등)
- **제약**: Dev 모드이므로 이전 대화 내역(History)은 반영되지 않습니다.
"""
)
def dev_reframing_gemma(
request: ReframingRequest,
service: LLMService = Depends(get_llm_service)
):
full_prompt = REFRAMING_PROMPT_TEMPLATE.format(
history_text="(없음. 대화 시작)",
user_input=request.user_input
)

raw_response = service.get_gemma_response(full_prompt)

try:
# 마크다운 코드블록 제거 (```json ... ```)
cleaned_json = raw_response.replace("```json", "").replace("```", "").strip()
data = json.loads(cleaned_json)

return ReframingResponse(
empathy=data.get("empathy", "공감 내용을 불러오지 못했습니다."),
detected_distortion=data.get("detected_distortion", "분석 불가"),
analysis=data.get("analysis", "분석 내용을 생성 중 오류가 발생했습니다."),
socratic_question=data.get("socratic_question", "질문을 생성하지 못했습니다."),
alternative_thought=data.get("alternative_thought", "대안을 찾지 못했습니다."),
emotion=data.get("top_emotion", None) # 감정 필드 추가
)
except json.JSONDecodeError:
logger.error(f"JSON 파싱 실패. 원본 응답: {raw_response}")
return ReframingResponse(
empathy=f"[JSON 파싱 실패] 모델 응답: {raw_response}",
detected_distortion="에러",
analysis="에러",
socratic_question="에러",
alternative_thought="에러"
)
72 changes: 0 additions & 72 deletions chatbot/controller/test_controller.py

This file was deleted.

4 changes: 2 additions & 2 deletions chatbot/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from starlette.exceptions import HTTPException as StarletteHTTPException
from fastapi.exceptions import RequestValidationError

from controller import chat_controller, search_controller, report_controller, test_controller
from controller import chat_controller, search_controller, report_controller, dev_controller

from exception import (
AppError,
Expand Down Expand Up @@ -44,7 +44,7 @@
app.include_router(search_controller.router)
app.include_router(chat_controller.router)
app.include_router(report_controller.router)
app.include_router(test_controller.router)
app.include_router(dev_controller.router)

@app.get("/chatbot/health", tags=["Health Check"])
def health_check():
Expand Down
2 changes: 1 addition & 1 deletion chatbot/prompts/mind_diary.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# chatbot/prompts/mind_diary.py

MIND_DIARY_PROMPT_TEMPLATE = """
당신은 전문 심리상담사이자 CBT(인지행동치료) 전문가입니다.
당신은 전문 심리상담사이자 CBT(인지행동치료) 전문가 '도란이'입니다.
사용자 '{user_name}'님이 작성한 '마음일기'를 읽고, 먼저 다가가서 대화를 시작해야 합니다.

[마음일기 정보]
Expand Down
1 change: 1 addition & 0 deletions chatbot/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pydantic
fastapi
uvicorn
mangum
openai>=1.0.0
# boto3 제거 (AWS 런타임 제공)
# google-cloud-aiplatform 제거 (Layer로 제공)
# google-auth 제거 (Layer로 제공)
66 changes: 64 additions & 2 deletions chatbot/service/llm_service.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# chatbot/service/llm_service.py
import json
import boto3
import logging
from botocore.exceptions import ClientError
from functools import lru_cache

from openai import OpenAI, OpenAIError
try:
from config import config
except ImportError:
Expand All @@ -25,13 +24,20 @@ class LLMService:
MODEL_ID_BEDROCK_EMBEDDING = 'amazon.titan-embed-text-v2:0'
MODEL_ID_GEMINI = "gemini-2.5-pro"

# vLLM에 로드된 모델명 (서버 로그나 curl /v1/models로 확인된 ID 사용)
# 만약 이름을 모르면 "/workspace/" 혹은 "default" 등을 시도
MODEL_ID_GEMMA = "0xMori/gemma-2-9b-safori-cbt-merged"

def __init__(self):
# AWS Client 초기화
self.bedrock_runtime = boto3.client(service_name='bedrock-runtime', region_name='ap-northeast-2')

# Vertex AI (Gemini) 초기화
self.gemini_pro_model = self._init_gemini()

# Hugging Face (vLLM) 클라이언트 초기화
self.hf_client = self._init_hf_client()

def _init_gemini(self):
if not vertexai:
logger.info("Gemini 라이브러리 없음")
Expand All @@ -54,6 +60,62 @@ def _init_gemini(self):
logger.error(f"Vertex AI 초기화 실패: {e}")
return None

def _init_hf_client(self):
"""
OpenAI 호환 클라이언트를 초기화합니다 (vLLM용)
"""
if not config.hf_endpoint_url or not config.hf_api_token:
logger.warning("HF Endpoint URL 또는 Token이 설정되지 않아 vLLM 클라이언트를 초기화하지 않습니다.")
return None

try:
# vLLM/OpenAI 호환 주소 처리: 끝에 /v1 붙이기
base_url = f"{config.hf_endpoint_url.rstrip('/')}/v1"

client = OpenAI(
base_url=base_url,
api_key=config.hf_api_token
)
logger.info("HF vLLM(OpenAI) 클라이언트 초기화 성공")
return client
except Exception as e:
logger.error(f"HF 클라이언트 초기화 실패: {e}")
return None

def get_gemma_response(self, prompt: str, max_tokens: int = 2048) -> str:
"""
vLLM에 배포된 Gemma 3 모델을 호출합니다. (OpenAI SDK 사용)
"""

if not self.hf_client:
error_msg = "오류: vLLM 클라이언트가 초기화되지 않았습니다. 설정을 확인하세요."
logger.error(error_msg)
return json.dumps({"empathy": error_msg}, ensure_ascii=False)

messages = [
{"role": "user", "content": prompt}
]

try:
response = self.hf_client.chat.completions.create(
model=self.MODEL_ID_GEMMA,
messages=messages,
max_tokens=max_tokens,
temperature=0.7,
top_p=0.9
)

result_text = response.choices[0].message.content
return result_text

except OpenAIError as e:
logger.error(f"vLLM 호출 오류 (OpenAI Error): {e}")
return json.dumps({"empathy": f"AI 모델 오류: {str(e)}"}, ensure_ascii=False)

except Exception as e:
logger.error(f"Gemma 연결 알 수 없는 오류: {e}")
return json.dumps({"empathy": f"시스템 오류: {str(e)}"}, ensure_ascii=False)

def get_embedding(self, text: str) -> list[float]:
body = json.dumps({"inputText": text})
try:
Expand Down
2 changes: 1 addition & 1 deletion chatbot/service/worker_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def _handle_mind_diary_event(payload: dict, repo: ChatRepository, llm) -> bool:
repo.log_cbt_session(
user_id=user_id,
session_id=new_session_id,
user_input="(마음일기 기반 선제 대화)", # 식별용 마커
user_input=content, # 식별용 마커
bot_response=final_bot_response,
embedding=[0.0] * 1024,
s3_url=s3_url
Expand Down
2 changes: 1 addition & 1 deletion chatbot/test/services/test_worker_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def test_process_sqs_mind_diary_event(mock_get_db_conn, mock_get_llm, MockChatRe
# 저장된 내용 검증
_, kwargs = mock_repo_instance.log_cbt_session.call_args
assert kwargs["user_id"] == "test_user"
assert kwargs["user_input"] == "(마음일기 기반 선제 대화)"
assert kwargs["user_input"] == "열심히 공부했는데 시험을 망쳐서 너무 슬퍼."
# 봇 응답의 empathy가 LLM 결과와 일치하는지 확인
assert kwargs["bot_response"]["empathy"] == "시험 때문에 많이 속상하셨겠어요."

Expand Down