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
34 changes: 34 additions & 0 deletions api/icebreaking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from fastapi import APIRouter
from pydantic import BaseModel
from typing import Optional, List
from service.icebreaking import get_icebreaking_response, IceBreakingState


router = APIRouter(tags=["Ice Breaking"])


class IceBreakingResponse(BaseModel):
state: Optional[IceBreakingState]
# continue: 이전 주제에 대한 이야기가 끝나지 않았으므로, 기존 주제를 계속 이어갑니다.
# switch: 이전 주제에 대한 이야기가 끝났으므로, 새로운 주제를 제시합니다.
# end: 모든 주제에 대한 이야기가 끝났으므로, 퇴장 메세지를 반환합니다.

# 실제 Ice Breaker가 전송할 메세지 본문 (switch, end)
text: Optional[str]


class IceBreakingRequest(BaseModel):
step: int
message_list: List[str]
room_id: str
icebreaker_message_list: List[str]


@router.post("/icebreaking/", response_model=IceBreakingResponse)
async def get_icebreaking_message(payload: IceBreakingRequest):
return get_icebreaking_response(
payload.step,
payload.message_list,
payload.room_id,
payload.icebreaker_message_list,
)
5 changes: 4 additions & 1 deletion config.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
USER_SERVER_URL: str
CHAT_SERVER_URL: str
OPENAI_API_KEY: str

class Config:
env_file = ".env"

settings = Settings()

settings = Settings()
3 changes: 2 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from fastapi import FastAPI
from api import health
from api import health, icebreaking

app = FastAPI(root_path="/ai")

app.include_router(health.router)
app.include_router(icebreaking.router)
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ fastapi
uvicorn
pydantic
pydantic-settings
requests
requests
openai=1.82.0
32 changes: 32 additions & 0 deletions service/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
GPT_MODEL = "gpt-4o"

SYSTEM_PROMPT = "너는 한국어 어르신 대화 코치이다. 반드시 순수 JSON 한 줄만 출력하세요."

ICEBREAKING_TOPICS = {
"자기소개": [
"이름 나이",
"거주지 & 고향",
"하루 일과",
],
"취미 & 관심사": [
"책",
"음식",
],
"추억": [
"학창 시절",
"직장 생활",
],
"지역 이야기": [
"동네 맛집 & 카페",
],
"건강": [
"현재 건강 상태",
],
"미래 계획": [
"올해 하고 싶은 일",
],
}

ICEBREAKER_MESSAGE_LIST_MAX_CNT = 9 # end state 결정 요소

END_TEXT = "서로 충분한 정서적 교류를 마쳤습니다. 저는 이만 나가보겠습니다."
160 changes: 160 additions & 0 deletions service/icebreaking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
from openai import OpenAI
from config import settings
import requests
import json
from enum import Enum
from service.constants import (
GPT_MODEL,
SYSTEM_PROMPT,
ICEBREAKING_TOPICS,
ICEBREAKER_MESSAGE_LIST_MAX_CNT,
END_TEXT,
)


class IceBreakingState(str, Enum):
CONTINUE = "continue"
SWITCH = "switch"
END = "end"


client = OpenAI(api_key=settings.OPENAI_API_KEY)


def is_end_state(icebreaker_message_list_cnt, state):
return (
icebreaker_message_list_cnt == ICEBREAKER_MESSAGE_LIST_MAX_CNT
and state == IceBreakingState.SWITCH
)


def get_icebreaking_response(step, message_list, room_id, icebreaker_message_list):

state = get_icebreaking_state(message_list, icebreaker_message_list)

# end state 예외 처리
if is_end_state(len(icebreaker_message_list), state):
state = IceBreakingState.END
text = END_TEXT
return {"state": state, "text": text}

# Ice Breaker가 전송할 메세지 본문
text = ""

# Ice Breaking 본문 생성
if state != "continue":
text = get_icebreaking_text(step, message_list, room_id)

return {"state": state, "text": text}


def get_icebreaking_text(step, message_list, room_id):

user_prompt = get_icebreaking_text_user_prompt(message_list)
response = get_gpt_api_response(user_prompt)
print(response)

try:
parsed = json.loads(response)
text = parsed["reply"]
except Exception as e:
print("GPT 응답 파싱 실패:", e)
text = "error"

url = f"{settings.USER_SERVER_URL}/message/iceBreakingTopic"
try:
payload = {
"roomId": room_id,
"stepNum": step + 1,
"content": text,
}
requests.post(url, json=payload)
except Exception as e:
print("메시지 저장 실패:", e)
return text


def get_icebreaking_state(message_list, icebreaker_message_list):

user_prompt = get_icebreaking_state_user_prompt(
message_list, icebreaker_message_list
)
response = get_gpt_api_response(user_prompt)
print(response)

try:
parsed = json.loads(response)
state = parsed["reply"]
except Exception as e:
print("GPT 응답 파싱 실패:", e)
state = "error"

return state


def get_gpt_api_response(user_prompt):
response = client.chat.completions.create(
model=GPT_MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
],
temperature=0.5,
)
return response.choices[0].message.content.strip()


def get_icebreaking_text_user_prompt(message_list):
return f"""
아래는 두 명의 어르신이 처음 만나 나누는 채팅방 대화 내용입니다.
당신은 해당 채팅방 안에서 이들의 어색함을 풀고 대화를 자연스럽게 이어가도록 돕는 한국어 어르신 대화 코치입니다.

당신의 역할는 다음과 같습니다:
1. 말문이 막힌 어르신의 입장에서, 대화를 자연스럽게 이어갈 수 있도록 따뜻한 주제를 추천합니다.
2. 말투는 정중하면서도 편안하게 유지해 주세요.
3. 반드시 제 3자 입장에서 답변해야 합니다. (답변할 문장을 직접 말하지 않습니다.)

당신의 목적은 다음과 같습니다:
1. 처음 만난 두 어르신이 어색함을 풀고 자연스럽게 대화를 이어가게 돕는다.
2. 서로의 정보를 알 수 있도록, 적절한 질문을 던진다.
3. 추천 질문은 어르신 간의 정서적 연결을 강화하는 것이어야 한다.

최근 메시지:
{message_list}

아이스브레이킹 추천 주제 리스트:
{ICEBREAKING_TOPICS}

다음 조건을 따르세요:
1. 최근 메세지를 바탕으로, 두 사람의 관계, 말투, 상황 맥락을 파악한다.
2. **아이스브레이킹 추천 주제 리스트**를 참고하여 총 3개의 주제를 생각한 뒤, **가장 자연스럽고 감정적 연결이 잘된 주제**를 선택하여 작성한다.

[좋은 예시] {{"reply":"첫 대화니, 이름부터 여쭤보는 것은 어떤가요?"}}
"""


def get_icebreaking_state_user_prompt(message_list, icebreaker_message_list):
return f"""
아래는 두 명의 어르신이 처음 만나 나누는 채팅방 대화 내용입니다.
당신은 해당 채팅방 안에서 이들의 어색함을 풀고 대화를 자연스럽게 이어가도록 돕는 한국어 어르신 대화 코치입니다.

최근 메세지:
{message_list}

이전에 제안한 메세지:
{icebreaker_message_list}

다음 조건을 따르세요:
1. 최근 메세지를 바탕으로, 두 사람의 관계, 말투, 상황 맥락을 파악합니다.
2. 이전에 제안한 주제에 대해 충분히 이야기를 나누었는지를 판단합니다.
3. 충분히 이야기를 나누었다면 **switch**, 아니라면 **continue**를 근거와 함께 반환합니다.

충분한 이야기를 나누었는지 판단하는 기준은 다음과 같습니다:
1. 두 어르신 모두 이전에 제안한 주제에 대해 1가지 이상을 답변했다.
2. 더 이상 질문이 오가지 않는다.
3. 반복되는 대답으로 정보가 고갈된 느낌이다.
4. 단답형 응답으로 대화가 마무리되었다.

[좋은 예시 1] {{"reply":"continue", "reason":"아직 persona_B가 답변하지 않았습니다."}}
[좋은 예시 2] {{"reply":"switch", "reason":"서로 주제에 대한 간단한 공유가 완료되었고, 자연스러운 회상이 이루어졌습니다."}}
"""
Loading