일상/컴퓨터

Open AI & Stremalit으로 캐릭터 챗봇 만들기

미적미적달팽이 2024. 8. 4. 13:24

stremlit은 python으로 홈페이지를 만들 수 있는 툴로 특별한 프론트 엔드 지식이 없어도 python에 대한 이해가 있으면 구현이 가능하다.

구글 코랩을 이용해 streamlit을 이용한 챗봇을 구현하고자 한다.

 

1. py 파일 만들기 & 기본 설정

colab에서 streamlit으로 홈페이지를 열기 전에 홈페이지 내부를 구성하는 .py 확장자 형식의 파일이 필요하다.

간단하게 사용가능한 고성능의 ai에 prompt engineering으로 fine-tuning 후 사용하는 방식으로 챗봇을 만들고자 한다.

구현을 위해 colab에는 먼저 필요한 라이브러리를 설치해야한다.

!pip install streamlit openai

colab의 로컬 directory에 streamlit으로 실행가능한 py 파일을 작성하고 저장하려면 코드셀 가장 첫번째 줄에 아래 코드를 추가하여 실행한다.

%%writefile 파일제목.py
import streamlit

또한 open ai의 api를 활용해서 제작을 할 것이기에 open ai api키를 입력해야 한다. api 키는 환경 변수로 전달 해야 하기 때문에 py 파일에 아래와 같은 코드를 추가해서 api가 py 파일 안에서 구동 될 수 있도록 한다.

from openai import OpenAI
import os

# 환경 변수 설정
os.environ["OPENAI_API_KEY"] = "api키"

# OpenAI API 키 설정
client = OpenAI(
   api_key=os.environ.get("OPENAI_API_KEY")
)

apen ai의 ai키는 https://platform.openai.com/api-keys 에서 생성해서 사용할 수 있다.

api를 사용하려면 추가적인 결제가 필요한데, 엄청난 작업을 할 것이 아닌 이상 매우 많은 요금을 결제할 필요는 없다. 또한 토큰 한도 초과 시 마다 얼마나 결제 될 지 제한을 둘 수 있기에 자신이 원하는 만큼 사용하면 된다. 결제 설정은 아래에서 가능하다.

https://platform.openai.com/settings/organization/billing/overview

 

 

2. 어떤 챗봇을 만들 것인가?

제타(zeta) - 상상이 현실이 되는 AI 채팅 https://zeta-ai.io/ko

요즘 LLM AI가 발달하면서 챗봇을 이용한 다양한 서비스가 출시되고 있으며 최근에는 캐릭터와 대화하는 챗봇이 나오고 있다.

단순하게 캐릭터 챗봇으로 대화하는 것은 다른 챗봇과 똑같으니 좀 더 나에게 도움(?)이 되도록 블로그 글을 작성을 돕는 챗봇을 만들어 볼까 한다.

open ai의 api에 프롬프트 엔지니어링을 통해 새로운 서비스를 제작하고자 할 때, 이용할 수 있는 방법은 코드에서 직접 프롬프트를 입력하거나, api에서 미리 설정을 한 후 fine-tuning한 모델을 사용하는 것이다.

gpt api에서 설정할 수 있는 목록은 아래와 같다.

  • Functions: API가 수행할 수 있는 특정 작업이나 동작. 이미지 생성, 텍스트 번역, 데이터 분석 등을 뜻함
  • Temperature: 생성 모델에서 출력의 다양성을 결정하는 값. 낮은 온도(예: 0.1)는 더 예측 가능하고 일관된 결과를, 높은 온도(예: 1.0)는 더 창의적이고 예측할 수 없는 결과를 생성.
  • Maximum Tokens: API 응답에서 생성할 수 있는 최대 토큰(단어 또는 문자 조각) 수. 예를 들어, 최대 토큰 수가 256이라면, 응답은 최대 256개의 토큰으로 구성.
  • Stop Sequences: 생성을 멈추게 할 특정 문자열 또는 시퀀스를 설정. 이 시퀀스가 텍스트에서 나타나면 모델은 그 지점에서 생성을 중단.
  • Top P: 이는 'nucleus sampling'이라고도 하는 확률 기준을 설정. 예를 들어 Top P가 1이면, 누적 확률이 1(100%)에 도달할 때까지 가능한 모든 토큰을 고려.
  • Frequency PenaltyPresence Penalty: 이 두 설정은 모델이 반복적으로 같은 단어나 구를 사용하는 것을 감소시키기 위해 사용. 'Frequency penalty'는 특정 단어가 이미 사용된 횟수에 따라 그 단어의 선택 확률을 감소. 'Presence penalty'는 단어가 한 번이라도 등장했을 때 그 단어의 재사용 확률을 감소.

open api reference에서 설명하는 json으로 설정할 수 있는 부분은 아래와 같다.

상위 레벨 항목

  • "id": "chatcmpl-abc123": 고유 식별자. 각 응답은 고유한 ID를 가짐.
  • "object": "chat.completion": 응답 객체의 유형. 여기서는 채팅 컴플리션(chat.completion)을 의미.
  • "created": 1677858242: 이 응답이 생성된 시간의 타임스탬프(유닉스 시간).
  • "model": "gpt-4o-mini": 이 요청에 사용된 GPT 모델의 이름. 여기서는 "gpt-4o-mini"라는 모델을 사용.

사용량(Usage) 정보

  • "usage": 요청과 응답에서 사용된 토큰 수.
    • "prompt_tokens": 13: 사용자가 입력한 프롬프트에서 사용된 토큰 수.
    • "completion_tokens": 7: GPT 모델이 생성한 응답에서 사용된 토큰 수.
    • "total_tokens": 20: 프롬프트와 응답을 합친 총 토큰 수. OpenAI API의 비용은 이 토큰 수에 따라 결정.

응답 내용(Choices)

  • "choices": 요청에 대한 응답의 선택지. 여기서는 단일 선택지만 포함.
    • "message": 응답 메시지.
      • "role": "assistant": 메시지를 생성한 역할. 여기서는 GPT 모델이 생성했으므로 "assistant".
      • "content": "\n\nThis is a test!": 모델이 생성한 응답의 실제 내용. 여기서는 "This is a test!"라는 문장이 응답으로 생성.
    • "logprobs": null: 로그 확률(logprobs) 필드는 일반적으로 모델이 각 토큰을 예측할 때의 확률을 나타내지만, 여기서는 사용되지 않음(null).
    • "finish_reason": "stop": 응답이 종료된 이유. "stop"은 모델이 더 이상 응답을 생성하지 않아 종료되었음을 의미.
    • "index": 0: 이 응답이 선택지 배열 내에서의 인덱스. 여기서는 단일 응답이므로 인덱스가 0.

prompt를 다 작성했으면 streamlit을 이용한 페이지를 좀 더 디자인 해보자.

html과 css의 기본적인 방식을 유사하게 사용해서 파이썬으로도 디자인이 가능하다. 기본적인 방법은 원하는

홈페이지 탭에 노출될 제목을 설정을 할 수 있다. 이때 이모티콘 을 삽입해서 홈페이지 아이콘을 설정할 수 있다.

st.set_page_config(page_title="페이지 제목", page_icon="아이콘")

streamlot에서 사용 가능한 아이콘 목록은 https://streamlit-emoji-shortcodes-streamlit-app-gwckff.streamlit.app/에서 확인이 가능하다.

 

streamlit app

App showing all the emoji shortcodes supported by Streamlit

streamlit-emoji-shortcodes-streamlit-app-gwckff.streamlit.app

 

다음은 페이지 상단에 노출될 제목을 설정해보자. 타이틀로 설정이 가능한데, 글자가 아닌 이미지로도 설정이 가능하다.

# Streamlit 애플리케이션 시작
st.title("페이지 제목")
# 타이틀 이미지 URL
title_image_url = "/content/image.png"

# Streamlit 애플리케이션 시작
st.markdown(f'<div class="title"><img src="{title_image_url}" alt="Title Image"></div>', unsafe_allow_html=True)

streamlit의 기본 chat bot ui보다 좀 더 챗봇 감성의 UI를 설정하기 위해 CSS와 HTML을 설정해준다.

# 말풍선 스타일의 메시지 표시 함수
def display_chat_message(role, content, avatar_url):
    bubble_class = "user-bubble" if role == "user" else "assistant-bubble"
    message_class = "user-message" if role == "user" else "assistant-message"
    st.markdown(f"""
    <div class="chat-bubble {bubble_class} {message_class}">
        <img src="{avatar_url}" class="chat-avatar">
        <div>{content}</div>
    </div>
    """, unsafe_allow_html=True)

사용자 언어에 맞게 한국어로 설정하고, 캐릭터 선택 단계로 넘어가기 위해 session 단계를 설정해준다. streamlit에서는 session 단계를 설정해서 다양한 기능을 구현할 수 있다.

# 세션 상태 초기화
if "messages" not in st.session_state:
    st.session_state.messages = []
    st.session_state.character = None
    st.session_state.language = "한국어"
    st.session_state.character_avatar_url = assistant_avatar_url
    st.session_state.stage = 1

streamlit에서 대화 히스토리가 나오게 하고 대화 생성 중에 로딩 표시가 나오도록 해서 디자인을 개선해보자.

# 대화 히스토리 표시
chat_container = st.empty()
with chat_container.container():
    st.markdown('<div class="chat-wrapper"><div class="chat-container">', unsafe_allow_html=True)
    for msg in st.session_state.messages:
        display_chat_message(msg["role"], msg["content"], st.session_state.character_avatar_url if msg["role"] == "assistant" else user_avatar_url)
    st.markdown('</div></div>', unsafe_allow_html=True)


# 대화 처리 단계
elif st.session_state.stage == 2:
    user_input = st.chat_input("대화를 입력하세요:", key="input_conversation")
    if user_input:
        st.session_state.messages.append({"role": "user", "content": user_input})
        with st.spinner('답변 생성 중... 잠시만 기다려 주세요.'):
            response = generate_conversation(st.session_state.language, st.session_state.character, user_input)
        st.session_state.messages.append({"role": "assistant", "content": response})

# 대화 히스토리 다시 표시
chat_container.empty()  # 이전 메시지 지우기
with chat_container.container():
    st.markdown('<div class="chat-wrapper"><div class="chat-container">', unsafe_allow_html=True)
    for msg in st.session_state.messages:
        display_chat_message(msg["role"], msg["content"], st.session_state.character_avatar_url if msg["role"] == "assistant" else user_avatar_url)
    st.markdown('</div></div>', unsafe_allow_html=True)

구현하고자 하는 기능은

  • 사용자가 원하는 애니 캐릭터의 말투로 대화
  • 블로그 글 주제 추천
  • 블로그 글 작성
  • 내용 개선

따라서 prompt를 아래와 같이 작성했다.

    1. 당신은 지금 {character}의 역할을 연기하고 있습니다. 사용자의의 요구와 질문에 {character}의 말투와 스타일로 한국어로 응답하세요.

    2. 다음은 애니 캐릭터에 대한 정보 링크입니다
    [케로로]: [https://namu.wiki/w/%EC%BC%80%EB%A1%9C%EB%A1%9C]. 
    [코난]: [https://namu.wiki/w/%EC%97%90%EB%8F%84%EA%B0%80%EC%99%80%20%EC%BD%94%EB%82%9C]
    이 정보를 바탕으로, 질문에 답하거나 이 캐릭터로 역할을 연기하세요.

    3. 사용자가 주제를 추천하길 원한다면, 최근 구글에서서 [특정 주제 분야, 예: 기술, 여행, 음식 등]와 관련된 인기 있는 주제를 검색하여 추천해 주세요.

    4. 다음은 사용자의 블로그 링크입니다: [https://gunrestaurant.tistory.com/]. 제공된 블로그의 글 스타일을 분석한 후, 사용자가 입력한 주제로 동일한 스타일의 블로그 글을 작성해 주세요. 글의 길이는 약 500 단어로 작성해 주세요.

    5. 사용자가 글의 개선하고 싶어하면 내용을 검토한 후, 명확성, 톤, 전반적인 품질을 향상시킬 수 있는 수정 사항을 제안해 주세요

선택할 때 버튼으로 선택하고 챗봇 ui 아바타 이미지에 해당 캐릭터 이미지가 나올 수 있도록 해준다.

# 애니 캐릭터와 그들의 정보 및 이미지 URL
characters = {
    "케로로": ["개구리중사", "https://i.namu.wiki/i/c1GTTKMxSQJhdu1ro8bu9KxQqe6csuMTxAA_V-TkxKS2D6CPzXFHXG8pG9PnAYeLFPOT-1vFSVDWmcEuT2fYTw.webp"],
    "코난": ["명탐정 코난", "https://i.namu.wiki/i/Tf8RK2-Zcr78evjdh9AzcnmL0c9KZvikc5mawtLMBjukOPqlJzRnN23MSJfwwNbvu0UGHyJjjnt3x519eqOA-g.webp"]
}

# 캐릭터 선택 단계
if st.session_state.stage == 1:
    selected_character = None
    st.markdown('<div class="member-selection">', unsafe_allow_html=True)
    st.markdown("<h3>캐릭터를 선택하세요:</h3>", unsafe_allow_html=True)
    for character, info in characters.items():
        character_key = f"button_{character}"
        if st.button(f"{character} 선택", key=f"{character_key}_button"):
            selected_character = character
            break
        st.markdown(f"""
        <div class="member-card" id="{character_key}">
            <img src="{info[1]}" class="chat-avatar">
            <span>{character}</span>
        </div>
        """, unsafe_allow_html=True)

    if selected_character:
        st.session_state.character = selected_character
        st.session_state.character_avatar_url = characters[selected_character][1]
        request_message = f"안녕하세요! {selected_character}입니다. 무엇을 도와드릴까요?"
        st.session_state.messages.append({"role": "assistant", "content": request_message})
        st.session_state.stage = 2
        st.rerun()

openai에 prompt를 넘기고 응답을 받기 위해 함수를 설정한다. 모델은 gpt 4o mini 모델을 사용했다.

# 대화를 생성하는 함수
def generate_conversation(language, character, user_input):
    prompt = f"""
    1. 당신은 지금 {character}의 역할을 연기하고 있습니다. 사용자의의 요구와 질문에 {character}의 말투와 스타일로 한국어로 응답하세요.

    2. 다음은 애니 캐릭터에 대한 정보 링크입니다
    [케로로]: [https://namu.wiki/w/%EC%BC%80%EB%A1%9C%EB%A1%9C]. 
    [코난]: [https://namu.wiki/w/%EC%97%90%EB%8F%84%EA%B0%80%EC%99%80%20%EC%BD%94%EB%82%9C]
    이 정보를 바탕으로, 질문에 답하거나 이 캐릭터로 역할을 연기하세요.

    3. 사용자가 주제를 추천하길 원한다면, 최근 구글에서서 [특정 주제 분야, 예: 기술, 여행, 음식 등]와 관련된 인기 있는 주제를 검색하여 추천해 주세요.

    4. 다음은 사용자의 블로그 링크입니다: [https://gunrestaurant.tistory.com/]. 제공된 블로그의 글 스타일을 분석한 후, 사용자가 입력한 주제로 동일한 스타일의 블로그 글을 작성해 주세요. 글의 길이는 약 500 단어로 작성해 주세요.

    5. 사용자가 글의 개선하고 싶어하면 내용을 검토한 후, 명확성, 톤, 전반적인 품질을 향상시킬 수 있는 수정 사항을 제안해 주세요

    사용자 입력: {user_input}
    """
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": prompt}
        ]
    )
    return response.choices[0].message.content

챗봇 스타일의 느낌을 더 살리기 위해 html과 CSS 스타일을 추가해준다.

# CSS 스타일 정의
def chat_styles():
    st.markdown("""
    <style>
    body, .stApp {
        background-color: white;
    }
    .stApp {
        color: black;
    }
    .title {
        color: black;
    }
    .title img {
        width: 100%;
        max-width: 300px;
        display: block;
        margin: 0 auto 20px auto;
    }
    .chat-bubble {
        padding: 10px;
        margin: 5px;
        border-radius: 10px;
        display: inline-block; /* 텍스트 길이에 맞춰 말풍선 길이 조정 */
        max-width: 70%;
        word-wrap: break-word;
        display: flex;
        align-items: flex-start;
    }
    .chat-avatar {
        width: 50px;
        height: 50px;
        border-radius: 50%;
        margin-right: 10px;
        object-fit: cover;
    }
    .user-bubble {
        background-color: #e0e0e0;
        color: black;
        border-top-right-radius: 0;
        margin-left: auto;
        flex-direction: row-reverse;
        gap: 10px;
    }
    .assistant-bubble {
        background-color: #d1a3ff;
        color: black;
        border-top-left-radius: 0;
        margin-right: auto;
        gap: 10px;
    }
    .user-message {
        align-self: flex-end;
    }
    .assistant-message {
        align-self: flex-start;
    }
    .spinner-container {
        display: flex;
        justify-content: center;
        align-items: center;
        margin: 10px 0;
    }
    .member-selection {
        display: flex;
        flex-direction: column;
        align-items: center;
    }
    .member-card {
        background-color: #f1f1f1;
        border: none;
        padding: 10px;
        margin: 5px;
        border-radius: 10px;
        display: flex;
        flex-direction: column;
        align-items: center;
        cursor: pointer;
        width: 200px;
        text-align: center;
    }
    .member-card img {
        border-radius: 50%;
        width: 100px;
        height: 100px;
        object-fit: cover;
        margin-bottom: 10px;
    }
    .member-card span {
        margin-bottom: 10px;
    }
    .member-button {
        background-color: #4CAF50;
        color: white;
        border: none;
        padding: 10px;
        text-align: center;
        text-decoration: none;
        display: inline-block;
        font-size: 14px;
        cursor: pointer;
        border-radius: 5px;
        width: 100%;
        box-sizing: border-box;
    }
    .member-card button {
        background-color: transparent;
        border: none;
        padding: 0;
        text-align: center;
        cursor: pointer;
    }
    </style>
    """, unsafe_allow_html=True)

# 말풍선 스타일의 메시지 표시 함수
def display_chat_message(role, content, avatar_url):
    bubble_class = "user-bubble" if role == "user" else "assistant-bubble"
    message_class = "user-message" if role == "user" else "assistant-message"
    st.markdown(f"""
    <div class="chat-bubble {bubble_class} {message_class}">
        <img src="{avatar_url}" class="chat-avatar">
        <div>{content}</div>
    </div>
    """, unsafe_allow_html=True)

 

3. 구글 코랩 환경 설정

구글 colab에서 streamlit을 실행하는 코드로 일반적으로 알려진 것은 아래와 같다.

!streamlit run app.py &>/content/logs.txt &
!npx localtunnel --port 8501

일반적으로 streamlit으로 홈페이지를 만들면 내 로컬서버로 외부에서 연결할 수 있는 port를 설정해서 호스팅을 하는 방법으로 홈페이지를 열 수 있다.

하지만 코랩에서는 위에 코드로 실행이 가능하나, 가끔씩 제대로 홈페이지로 접속할 수 있는 port 연결이 제대로 되지 않기에 다른 방법으로 설정을 해야 지속적으로 사용이 가능하다.

이를 위해 외부에서 연결할 로컬로 연결할 수 있게 도와주는 프로그램인 ngrok 프로그램을 이용해서 코랩 환경을 이용해서 지속적으로 홈페이지를 연결할 수 있도록 한다.

먼저 ngrok 홈페이지에 접속해서 가입을 한다.

https://dashboard.ngrok.com/get-started/setup/windows

 

ngrok - Online in One Line

 

dashboard.ngrok.com

 

colab에서는 pyngrok을 설치를 하고 라이브러리를 불러와 준다.

!pip install pyngrok
from pyngrok import ngrok

ngrok 홈페이지의 dashboard에서 영어와 숫자로 생성된 자신의 토큰이 보일 것이다. 복사해서 아래의 코드에 넣어서 실행을 한다.
ngrok - Online in One Line dashboard.ngrok.com

 

ngrok - Online in One Line

 

dashboard.ngrok.com

 

ngrok.set_auth_token("자신의 토큰")

streamlit으로 코드 파일을 실행한다.

!streamlit run app.py --server.port 8501 &>/dev/null&

이때 이전에 가끔 너무 많이 실행했거나 제대로 실행이 됐는지 확인이 필요할 수 있는데, 이때 아래와 같은 코드를 실행한다.

from pyngrok import ngrok

# 현재 실행 중인 모든 터널을 가져오기
tunnels = ngrok.get_tunnels()

# 각 터널 종료
for tunnel in tunnels:
    public_url = tunnel.public_url
    ngrok.disconnect(public_url)

!lsof -i :8501


!curl http://localhost:8501

!curl http://localhost:8501을 실행했을 때 페이지 html 내용이 나오면 제대로 페이지가 생성된 것이다.

아래 코드를 실행해서 나오는 홈페이지로 들어간다.

public_url = ngrok.connect(addr="8501")
print(public_url)

완성된 페이지는 아래와 같이 나온다.

전체 코드는 아래와 같다.

%%writefile app.py
import streamlit as st
from openai import OpenAI
import os

st.session_state.language = '한국어'

# Streamlit 설정
st.set_page_config(page_title="블로그 도와줘!", page_icon=":house_with_garden:")

# 환경 변수 설정
os.environ["OPENAI_API_KEY"] = ""

# OpenAI API 키 설정
client = OpenAI(
   api_key=os.environ.get("OPENAI_API_KEY")
)

# 애니 캐릭터와 그들의 정보 및 이미지 URL
characters = {
    "케로로": ["개구리중사", "https://i.namu.wiki/i/c1GTTKMxSQJhdu1ro8bu9KxQqe6csuMTxAA_V-TkxKS2D6CPzXFHXG8pG9PnAYeLFPOT-1vFSVDWmcEuT2fYTw.webp"],
    "코난": ["명탐정 코난", "https://i.namu.wiki/i/Tf8RK2-Zcr78evjdh9AzcnmL0c9KZvikc5mawtLMBjukOPqlJzRnN23MSJfwwNbvu0UGHyJjjnt3x519eqOA-g.webp"]
}

# 사용자 아바타 이미지 URL
user_avatar_url = "https://via.placeholder.com/50?text=User"
assistant_avatar_url = "https://via.placeholder.com/50?text=Bot"

# CSS 스타일 정의
def chat_styles():
    st.markdown("""
    <style>
    body, .stApp {
        background-color: white;
    }
    .stApp {
        color: black;
    }
    .title {
        color: black;
    }
    .title img {
        width: 100%;
        max-width: 300px;
        display: block;
        margin: 0 auto 20px auto;
    }
    .chat-bubble {
        padding: 10px;
        margin: 5px;
        border-radius: 10px;
        display: inline-block; /* 텍스트 길이에 맞춰 말풍선 길이 조정 */
        max-width: 70%;
        word-wrap: break-word;
        display: flex;
        align-items: flex-start;
    }
    .chat-avatar {
        width: 50px;
        height: 50px;
        border-radius: 50%;
        margin-right: 10px;
        object-fit: cover;
    }
    .user-bubble {
        background-color: #e0e0e0;
        color: black;
        border-top-right-radius: 0;
        margin-left: auto;
        flex-direction: row-reverse;
        gap: 10px;
    }
    .assistant-bubble {
        background-color: #d1a3ff;
        color: black;
        border-top-left-radius: 0;
        margin-right: auto;
        gap: 10px;
    }
    .user-message {
        align-self: flex-end;
    }
    .assistant-message {
        align-self: flex-start;
    }
    .spinner-container {
        display: flex;
        justify-content: center;
        align-items: center;
        margin: 10px 0;
    }
    .member-selection {
        display: flex;
        flex-direction: column;
        align-items: center;
    }
    .member-card {
        background-color: #f1f1f1;
        border: none;
        padding: 10px;
        margin: 5px;
        border-radius: 10px;
        display: flex;
        flex-direction: column;
        align-items: center;
        cursor: pointer;
        width: 200px;
        text-align: center;
    }
    .member-card img {
        border-radius: 50%;
        width: 100px;
        height: 100px;
        object-fit: cover;
        margin-bottom: 10px;
    }
    .member-card span {
        margin-bottom: 10px;
    }
    .member-button {
        background-color: #4CAF50;
        color: white;
        border: none;
        padding: 10px;
        text-align: center;
        text-decoration: none;
        display: inline-block;
        font-size: 14px;
        cursor: pointer;
        border-radius: 5px;
        width: 100%;
        box-sizing: border-box;
    }
    .member-card button {
        background-color: transparent;
        border: none;
        padding: 0;
        text-align: center;
        cursor: pointer;
    }
    </style>
    """, unsafe_allow_html=True)

# 말풍선 스타일의 메시지 표시 함수
def display_chat_message(role, content, avatar_url):
    bubble_class = "user-bubble" if role == "user" else "assistant-bubble"
    message_class = "user-message" if role == "user" else "assistant-message"
    st.markdown(f"""
    <div class="chat-bubble {bubble_class} {message_class}">
        <img src="{avatar_url}" class="chat-avatar">
        <div>{content}</div>
    </div>
    """, unsafe_allow_html=True)

# 대화를 생성하는 함수
def generate_conversation(language, character, user_input):
    prompt = f"""
    1. 당신은 지금 {character}의 역할을 연기하고 있습니다. 사용자의의 요구와 질문에 {character}의 말투와 스타일로 한국어로 응답하세요.

    2. 다음은 애니 캐릭터에 대한 정보 링크입니다
    [케로로]: [https://namu.wiki/w/%EC%BC%80%EB%A1%9C%EB%A1%9C].
    [코난]: [https://namu.wiki/w/%EC%97%90%EB%8F%84%EA%B0%80%EC%99%80%20%EC%BD%94%EB%82%9C], 말투는 반말로 해주세요.
    [코난]의 명대사: [https://namu.wiki/w/%EB%AA%85%ED%83%90%EC%A0%95%20%EC%BD%94%EB%82%9C/%EB%AA%85%EB%8C%80%EC%82%AC]
    이 정보를 바탕으로, 질문에 답하거나 이 캐릭터로 역할을 연기하세요.

    3. 사용자가 주제를 추천하길 원한다면, 최근 구글에서서 [특정 주제 분야, 예: 기술, 여행, 음식 등]와 관련된 인기 있는 주제를 검색하여 추천해 주세요.

    4. 다음은 사용자의 블로그 링크입니다: [https://gunrestaurant.tistory.com/]. 제공된 블로그의 글 스타일을 분석한 후, 사용자가 입력한 주제로 동일한 스타일의 블로그 글을 작성해 주세요. 글의 길이는 약 500 단어로 작성해 주세요.

    5. 사용자가 글의 개선하고 싶어하면 내용을 검토한 후, 명확성, 톤, 전반적인 품질을 향상시킬 수 있는 수정 사항을 제안해 주세요

    사용자 입력: {user_input}
    """
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": prompt}
        ]
    )
    return response.choices[0].message.content

# Streamlit 애플리케이션 시작
st.title("블로그 도와줘!")

# CSS 스타일 적용
chat_styles()

# 세션 상태 초기화
if "messages" not in st.session_state:
    st.session_state.messages = []
    st.session_state.character = None
    st.session_state.language = "한국어"
    st.session_state.character_avatar_url = assistant_avatar_url
    st.session_state.stage = 1

# 대화 히스토리 표시
chat_container = st.empty()
with chat_container.container():
    st.markdown('<div class="chat-wrapper"><div class="chat-container">', unsafe_allow_html=True)
    for msg in st.session_state.messages:
        display_chat_message(msg["role"], msg["content"], st.session_state.character_avatar_url if msg["role"] == "assistant" else user_avatar_url)
    st.markdown('</div></div>', unsafe_allow_html=True)

# 캐릭터 선택 단계
if st.session_state.stage == 1:
    selected_character = None
    st.markdown('<div class="member-selection">', unsafe_allow_html=True)
    st.markdown("<h3>캐릭터를 선택하세요:</h3>", unsafe_allow_html=True)
    for character, info in characters.items():
        character_key = f"button_{character}"
        if st.button(f"{character} 선택", key=f"{character_key}_button"):
            selected_character = character
            break
        st.markdown(f"""
        <div class="member-card" id="{character_key}">
            <img src="{info[1]}" class="chat-avatar">
            <span>{character}</span>
        </div>
        """, unsafe_allow_html=True)

    if selected_character:
        st.session_state.character = selected_character
        st.session_state.character_avatar_url = characters[selected_character][1]
        request_message = f"안녕하세요! {selected_character}입니다. 무엇을 도와드릴까요?"
        st.session_state.messages.append({"role": "assistant", "content": request_message})
        st.session_state.stage = 2
        st.rerun()

# 대화 처리 단계
elif st.session_state.stage == 2:
    user_input = st.chat_input("대화를 입력하세요:", key="input_conversation")
    if user_input:
        st.session_state.messages.append({"role": "user", "content": user_input})
        with st.spinner('답변 생성 중... 잠시만 기다려 주세요.'):
            response = generate_conversation(st.session_state.language, st.session_state.character, user_input)
        st.session_state.messages.append({"role": "assistant", "content": response})

# 대화 히스토리 다시 표시
chat_container.empty()  # 이전 메시지 지우기
with chat_container.container():
    st.markdown('<div class="chat-wrapper"><div class="chat-container">', unsafe_allow_html=True)
    for msg in st.session_state.messages:
        display_chat_message(msg["role"], msg["content"], st.session_state.character_avatar_url if msg["role"] == "assistant" else user_avatar_url)
    st.markdown('</div></div>', unsafe_allow_html=True)
반응형