Skip to content

[2024/07/25 - 2024/07/27] 선택한 버튼에 따라 다른 프롬프트 제공 - 쿼리스트링 #17

Description

@SeoMiYoung

🤔 어떤 작업을 해야하나..

결국, 내 생각에는 모델은 하나만 사용하고, 선택한 상황에 따라 다른 프롬프트를 제공하는게 맞을 것 같다는 생각이 들었다.

현재 사용자가 선택할 수 있는 사항은 4가지로 해놓았는데,

  1. selectedGameType: 어떤 놀이인지(ex. 병원 놀이)
  2. selectedUserRole: 사용자의 역할은 무엇인지(ex. 의사)
  3. selectedAIRole: AI의 역할은 무엇인지(ex. AI)
  4. selectedAIVoice: AI의 음성은 무엇으로 할 것인지(ex. 음성1)

=> 이 4가지 선택지를 기반으로 fastAPI는 LM Studio에 응답을 요청할 때, 다른 prompt를 제공해야한다.
바로, fastAPI에서 # 프롬프트 내용 부분에 다른 요청을 해야된다는 소리이다.

prompt_template = f"""
        # 프롬프트 내용

        #Question:
        {message.question}

        #Answer: """

예를 들어 병원 놀이라면, "AI야 너는 지금 병원놀이를 하고 있어. 너의 역할은 환자야. 그리고 사용자는 의사야~"이런식으로 말이다. 이렇게 사용자의 선택지에 따라서 다른 프롬프트를 제공해야한다.
아 음성은 프롬프트와 상관없을수도 있겠지만, 일단은 포함시켰다.

🔶 어떻게 4가지 정보를 전달할 것인가..

찾아본 결과, 서버로 데이터를 보내는 몇가지 방법들을 찾을 수 있었다.

서버로 데이터를 보내는 방법에는 여러가지가 있다. 일반적으로 사용되는 세 가지 방법은 다음과 같다.
(내가 백엔드를 잘 몰라서...정확하지 않을수도 있다.. 문제가 있다면 알려주길!!)

🔸 (1) url parameter

URL 파라미터는 RESTful API에서 자주 사용되는 방법으로, 리소스를 식별하거나 특정 작업을 지정할 때 사용됩니다. URL 경로의 일부분으로 포함되며, 보통 슬래시 /로 구분됩니다.

GET /users/123

여기서 123은 특정 사용자 리소스를 식별하는 파라미터입니다.

🔸 (2) query string

쿼리 스트링은 URL의 끝에 ?뒤에 키-값쌍으로 데이터를 포함시키는 방법입니다. 여러 개의 파라미터는 &로 구분됩니다.

GET /search?query=example&limit=10&page=2

위의 예시에서 query, limit, page는 쿼리 스트링 파라미터입니다. 이 방법은 주로 GET요청에서 데이터를 보낼 때 사용됩니다.

🔸 (3) 요청 부문(body)에 데이터를 포함시키기

요청 본문은 POST, PUT, PATCH 등의 요청에서 주로 사용되며, 데이터는 HTTP 메시지의 본문에 포함됩니다. JSON, XML, 폼 데이터 등 다양한 포맷을 사용할 수 있습니다.

POST /users
Content-Type: application/json

{
  "name": "John Doe",
  "email": "john.doe@example.com"
}

위의 예시에서는 JSON형식으로 사용자 정보를 본문에 포함시켜 보냅니다.
더 궁금하면 해당글참고

🔸 그 외의 방법

사실 그 외에도 HTTP의 Header에 데이터를 포함시켜 보내는 방법, 데이터를 쿠키에 포함시켜 보내는 방법 등 여러 방법들이 있으나, 위에서 언급한 3가지 방법이 일반적입니다.

🔶 어떤 방법을 선택해야할까?

url parameter ✔️ 추천할 때
- 리소스를 식별하거나 특정 자원을 참조할 때
- RESTful API에서 리소스의 ID 등을 전달할 때

✔️ 장점
- URL 자체에 의미를 부여할 수 있어 직관적임
- 리소스 식별에 용이함

✔️ 단점
- 데이터 길이가 제한적임
- 민감한 데이터를 포함하기 어려움
쿼리 스트링 ✔️ 추천할 때
- 검색 필터, 페이지네이션 등에서 여러 개의 파라미터를 전달할 때
- GET 요청에서 데이터를 전달할 때

✔️ 장점
- 어러 개의 키-값 쌍을 쉽게 전달 가능
- URL에 포함되어 브라우저 히스토리에 저장 가능

✔️ 단점
- 데이터의 길이가 제한적임
- 민감한 데이터를 포함하기 어려움
요청 본문 ✔️ 추천할 때
- 데이터를 생성하거나 업데이트할 때 (POST, PUT, PATCH 요청)
- 대량의 데이터 또는 복잡한 데이터 구조를 전달할 때

✔️ 장점
- 데이터 길이에 제한이 거의 없음
- 구조화된 데이터(JSON, XML 등)를 전달하기에 적합함

✔️ 단점
- GET 요청에는 사용되지 않음
- URL에 포함되지 않아, 브라우저 히스토리에 저장되지 않음

image
나는 위와 같은 이유로, 쿼리 스트링 방법이 적합하다고 생각했는데, 다른 방법이 더 적합해 보이면 피드백 해줄것!!!

🔶 쿼리 스트링 방법을 썼을 때, 코드

다음 3개 파일의 코드 변경이 생겼다.

(1) Server1.py

from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from langchain_openai import ChatOpenAI
from langchain_core.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser

app = FastAPI()

# CORS 설정
origins = [
    "http://localhost",
    "http://localhost:3000",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["POST"],
    allow_headers=["Content-Type"],
)

llm = ChatOpenAI(
    base_url="http://localhost:5000/v1",
    api_key="lm-studio",
    model="teddylee777/Llama-3-Open-Ko-8B-gguf",
    streaming=True,
    callbacks=[StreamingStdOutCallbackHandler()],
)

class Message(BaseModel):
    question: str

@app.post("/chat")
async def chat_with_bot(
    message: Message,
    selectedGameType: str = Query(...),
    selectedUserRole: str = Query(...),
    selectedAIRole: str = Query(...),
    selectedAIVoice: str = Query(...)
):
    try:
        prompt_template = f"""
        You are the assistant, you are the {selectedAIRole}. 
        {selectedGameType} will deal with {selectedUserRole}. 
        {selectedAIRole} must answer {selectedUserRole} in simple expressions and informal language.

        #Question:
        {message.question}

        #Answer: """

        prompt = PromptTemplate.from_template(prompt_template)
        chain = prompt | llm | StrOutputParser()

        response = chain.invoke({"question": message.question})
        return {"response": response}

    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

[ CORS 설정 ]

CORS 설정에서 origins는 특정 출처로부터의 요청을 허용하도록 설정하는 부분입니다.

  • http://localhost
    • 이 출처에서 오는 모든 요청을 허용한다는 의미입니다. 이는 일반적으로 백엔드 서버와 같은 출처에 있는 클라이언트 애플리케이션(ex. 프론트)에서 오는 요청을 허용하는 데 사용됩니다.
  • http://localhost:3000
    • 3000번 포트는 현재 리액트 앱을 의미하며, 즉 프론트 측에서 오는 요청을 허용한다는 의미입니다.

CORS 설정이 없는 경우에 브라우저가 차단하는 것은 "다른 출처"에서 오는 요청입니다. "다른 출처"란 도메인, 프로토콜, 또는 포트가 다른 경우를 말합니다.

[ fastAPI 애플리케이션에 CORS 미들웨어 추가 ]

이 코드는 FastAPI 애플리케이션에 CORS 미들웨어를 추가하여 특정 출처에서 오는 요청을 허용하는 설정을 적용하는 부분입니다. CORS 미들웨어는 웹 브라우저가 서로 다른 출처에서 오는 요청을 차단하지 않고 허용하도록 도와줍니다.

이 코드의 목적은 http://localhost:3000에서 오는 요청을 허용하고, 그 요청이 자격 증명을 포함할 수 있도록 하며, POST 메서드와 Content-Type 헤더를 사용할 수 있게 설정하는 것입니다. 이를 통해 로컬에서 실행되는 프런트엔드 애플리케이션이 FastAPI 백엔드와 원활하게 통신할 수 있도록 합니다.

# CORS 미들웨어 추가
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,  # 허용할 출처 리스트
    allow_credentials=True,  # 자격 증명(쿠키, 인증 헤더 등) 포함 요청 허용
    allow_methods=["POST"],  # 허용할 HTTP 메서드 (예: GET, POST 등)
    allow_headers=["Content-Type"],  # 허용할 HTTP 헤더
)

[ ChatOpenAI라는 클래스를 사용하여 LM을 설정 ]

llm = ChatOpenAI(
    base_url="http://localhost:5000/v1",
    api_key="lm-studio",
    model="teddylee777/Llama-3-Open-Ko-8B-gguf",
    streaming=True,
    callbacks=[StreamingStdOutCallbackHandler()],
)

이 코드는 ChatOpenAI라는 클래스를 사용하여 Language Model(LM)을 설정하는 부분입니다. 여기서는 ChatOpenAI를 인스턴스화하여 로컬 서버에서 실행되는 언어 모델 API와 상호 작용하도록 설정하고 있습니다. 이 인스턴스를 통해 AI 챗봇의 대화를 처리하는 역할을 합니다.

(2) Chat.jsx

import React, { useState } from 'react';
import Layout from '../../components/Layout/Layout';
import { useNavigate } from 'react-router-dom';
import Alert from '../../components/Alert/Alert';
import styles from './Chat.module.scss';
import { useLocation } from 'react-router-dom';

function Chat() {
    const location = useLocation();
    const queryParams = new URLSearchParams(location.search);
    const selectedGameType = queryParams.get('selectedGameType');
    const selectedUserRole = queryParams.get('selectedUserRole');
    const selectedAIRole = queryParams.get('selectedAIRole');
    const selectedAIVoice = queryParams.get('selectedAIVoice');

    const [messages, setMessages] = useState([]);
    const [userMessage, setUserMessage] = useState('');
    const [loading, setLoading] = useState(false);
    const [showAlert, setShowAlert] = useState(false);
    const [isListening, setIsListening] = useState(false);
    const navigate = useNavigate();

    const handleInputChange = (e) => {
        setUserMessage(e.target.value);
    };

    const speakText = (text) => {
        const utterance = new SpeechSynthesisUtterance(text);
        utterance.lang = 'ko-KR';
        window.speechSynthesis.speak(utterance);
    };

    const handleSendMessage = async () => {
        if (!userMessage.trim()) return;

        const newMessages = [...messages, { sender: '사용자', text: userMessage }];
        setMessages(newMessages);
        setUserMessage('');
        setLoading(true);

        try {
            const response = await fetch(`http://localhost:8000/chat?selectedGameType=${selectedGameType}&selectedUserRole=${selectedUserRole}&selectedAIRole=${selectedAIRole}&selectedAIVoice=${selectedAIVoice}`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ question: userMessage })
            });

            const data = await response.json();
            const computerMessage = { sender: '컴퓨터', text: data.response };
            setMessages([...newMessages, computerMessage]);
            speakText(computerMessage.text);
        } catch (error) {
            console.error('Error:', error);
            setMessages([...newMessages, { sender: '컴퓨터', text: 'Error: 응답을 가져올 수 없습니다.' }]);
        } finally {
            setLoading(false);
        }
    };

    const handleStartListening = () => {
        if (!('webkitSpeechRecognition' in window)) {
            alert('STT를 지원하지 않는 브라우저입니다.');
            return;
        }

        const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
        recognition.lang = 'ko-KR';
        recognition.interimResults = false;
        recognition.onresult = (event) => {
            const transcript = event.results[0][0].transcript;
            setUserMessage(transcript);
            handleSendMessage();
        };

        recognition.onend = () => {
            setIsListening(false);
        };

        recognition.start();
        setIsListening(true);
    };

    const handleStopClick = () => {
        setShowAlert(true);
    };

    const handleCloseAlert = () => {
        setShowAlert(false);
    };

    const handleConfirmExit = () => {
        navigate('/result');
    };

    return (
        <Layout>
            <div className={styles.chatContainer}>
                <div className={styles.chatWrapper}>
                    <div className={styles.chatHeader}>
                        <h1>역할놀이 챗봇</h1>
                        <button className={styles.stopButton} onClick={handleStopClick}>
                            X
                        </button>
                    </div>
                    <div className={styles.chatBody}>
                        {messages.map((msg, index) => (
                            <div key={index} className={`${styles.message} ${msg.sender === '사용자' ? styles.userMessage : styles.computerMessage}`}>
                                <strong>{msg.sender}:</strong> {msg.text}
                            </div>
                        ))}
                    </div>
                    <div className={styles.userInput}>
                        <input
                            type="text"
                            id="user-message"
                            value={userMessage}
                            onChange={handleInputChange}
                            placeholder="메시지를 입력하세요..."
                            className={styles.userMessageInput}
                        />
                        <button className={styles.sendButton} onClick={handleSendMessage}>
                            전송
                        </button>
                        <button className={styles.sendButton} onClick={handleStartListening} disabled={isListening}>
                            {isListening ? '음성 인식 중...' : '음성 입력'}
                        </button>
                    </div>
                    {loading && <div className={styles.loading}>로딩 중...</div>}
                </div>
                {showAlert && (
                    <Alert
                        message="정말로 종료하시겠습니까?"
                        onConfirm={handleConfirmExit}
                        onCancel={handleCloseAlert}
                    />
                )}
            </div>
        </Layout>
    );
}

export default Chat;

(3) Situation.jsx

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Layout from '../../components/Layout/Layout';
import Alert from '../../components/Alert/Alert';
import styles from './Situation.module.scss';

function Situation() {
    const navigate = useNavigate();
    const [selectedGameType, setSelectedGameType] = useState(null);
    const [selectedUserRole, setSelectedUserRole] = useState(null);
    const [selectedAIRole, setSelectedAIRole] = useState(null);
    const [selectedAIVoice, setSelectedAIVoice] = useState(null);
    const [rightPanelState, setRightPanelState] = useState(false);
    const [showAlert, setShowAlert] = useState(false);

    const roleOptions = {
        '학교 놀이': ['선생님', '학생', '교장선생님'],
        '병원 놀이': ['의사', '환자', '간호사'],
        '시장 놀이': ['상인', '소비자', '경찰'],
        '가족 놀이': ['아빠', '엄마', '아들', '딸'],
        '소꿉 놀이': ['너구리', '사자', '토끼'],
        '소방관 놀이': ['소방관', '도움이 필요한 사람'],
        '연예인 놀이': ['가수', '배우', '메이크업 담당 선생님', '헤어 담당 선생님', '매니저'],
        '승무원 놀이': ['승객', '승무원'],
    };

    const voiceOptions = ['음성A', '음성B', '음성C', '음성D'];

    const handleRolePlaySelect = (gameType) => {
        if (gameType === selectedGameType) {
            setSelectedGameType(null);
            setRightPanelState(false);
        } else {
            setSelectedGameType(gameType);
            setRightPanelState(true);
        }
    };

    const handleNextStep = () => {
        if (selectedGameType && selectedUserRole && selectedAIRole && selectedAIVoice) {
            const queryParams = new URLSearchParams({
                selectedGameType,
                selectedUserRole,
                selectedAIRole,
                selectedAIVoice
            }).toString();
            navigate(`/chat?${queryParams}`);
        } else {
            setShowAlert(true);
        }
    };
    

    const handleExit = () => {
        navigate('/');
    };

    const handleCloseAlert = () => {
        setShowAlert(false);
    };

    return (
        <Layout>
            <div className={styles.situationWrap}>
                <div className={styles.header}>
                    <h1 className={styles.title}>Roleplay With AI</h1>
                    <button onClick={handleExit} className={styles.exitButton}>돌아가기</button>
                </div>
                <div className={styles.content}>
                    <div className={styles.leftPanel}>
                        <h2 className={styles.subtitle}>놀이를 선택해주세요</h2>
                        <div className={styles.gameTypeBox}>
                            {Object.keys(roleOptions).map((gameType) => (
                                <button
                                    key={gameType}
                                    onClick={() => handleRolePlaySelect(gameType)}
                                    className={`${styles.gameTypeButton} ${gameType === selectedGameType ? styles.selected : ''}`}
                                >
                                    {gameType}
                                </button>
                            ))}
                        </div>
                    </div>
                    {rightPanelState && selectedGameType && (
                        <div className={styles.rightPanel}>
                            <div className={styles.rightPanelHeader}>
                                <h3>세부 설정</h3>
                                <p>선택된 놀이: {selectedGameType}</p>
                            </div>
                            <div className={styles.selectionSection}>
                                <h3>🐻 사용자 역할</h3>
                                <select onChange={(e) => setSelectedUserRole(e.target.value)} className={styles.selectDropdown}>
                                    <option value="">선택해주세요</option>
                                    {roleOptions[selectedGameType].map((role) => (
                                        <option key={role} value={role}>{role}</option>
                                    ))}
                                </select>
                            </div>
                            <div className={styles.selectionSection}>
                                <h3>🐻 AI 역할</h3>
                                <select onChange={(e) => setSelectedAIRole(e.target.value)} className={styles.selectDropdown}>
                                    <option value="">선택해주세요</option>
                                    {roleOptions[selectedGameType].map((role) => (
                                        <option key={role} value={role}>{role}</option>
                                    ))}
                                </select>
                            </div>
                            <div className={styles.selectionSection}>
                                <h3>🐻 AI 음성</h3>
                                <select onChange={(e) => setSelectedAIVoice(e.target.value)} className={styles.selectDropdown}>
                                    <option value="">선택해주세요</option>
                                    {voiceOptions.map((voice) => (
                                        <option key={voice} value={voice}>{voice}</option>
                                    ))}
                                </select>
                            </div>
                            <div className={styles.nextButtonContainer}>
                                <button onClick={handleNextStep} className={styles.nextButton}>다음 단계</button>
                            </div>
                        </div>
                    )}
                </div>
                {showAlert && (
                    <Alert
                        message="모든 항목을 선택해야 합니다."
                        onConfirm={handleCloseAlert}
                    />
                )}
            </div>
        </Layout>
    );
}

export default Situation;

🔶 쿼리 스트링으로 짰을 때, 실행 결과이다.

응답을 받는데 시간이 꽤 걸리는데, 참을성을 가지고 보시오!
Animation

실행결과가 진행될 때, 상단의 url을 확인해보면, 채팅페이지로 넘어갈때 다음과 같은 url로 설정되는 걸 확인할 수 있다.

image

http://localhost:3000/chat?selectedGameType=%EC%8B%9C%EC%9E%A5+%EB%86%80%EC%9D%B4&selectedUserRole=%EC%86%8C%EB%B9%84%EC%9E%90&selectedAIRole=%EC%83%81%EC%9D%B8&selectedAIVoice=%EC%9D%8C%EC%84%B1A

상단의 url 보이시나요? 복사 붙혀넣기 했더니 한글 대신 이상한 문자들이 포함되는 걸 확인할 수 있을텐데, 한글이 url에 포함될 때, url 인코딩이라는 과정을 거치게 되기 때문입니다. url 인코딩은 특수 문자 및 비-ASCII 문자를 퍼센트 기호(%)와 16진수 코드로 변환하는 방법입니다. 이로 인해 한글 등 비-ASCII 문자가 포함된 URL이 인코딩된 형태로 보이게 됩니다.

예를 들어, "시장 놀이"는 "%EC%8B%9C%EC%9E%A5+%EB%86%80%EC%9D%B4"로 인코딩됩니다. 이는 브라우저가 한글을 URL에 포함할 때 자동으로 인코딩하기 때문입니다.

암튼 URL 인코딩이 중요한게 아니라, 제가 하고 싶은말은 저렇게 쿼리 스트링 방식으로 다른 프롬프트를 제공할 수 있다 이말입니다.

✏️ 해당 이슈와 관련되서 밸류업 해야할 사항들

  • 일단 쿼리스트링 방식을 백엔드를 배운 팀원들에게 물어봐야겠다. 의견을 구해보고, 더 나은 방법이 있는건지, 어떤식으로 전달하면 좋을지 의견을 구해봐야겠다.
  • 프롬프트를 어떤식으로 설정해야할지 프롬프트 엔지니어링 부분에서 더 공부해야될 것 같다.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions