이번 시간에는 현재 구현된 AI Chatbot 시스템에서 기존에 작성된 OpenAI Instruction 에 추가로 특정 문구를 추가하여 추천 질문을 사용자에게 전달할 수 있는 시스템을 구현한 내용을 기반으로 블로그 포스팅을 진행하도록 하겠습니다. 

추천 질문 시스템

해당 게시글에서 다루는 추천 질문 시스템은 사용자가 AI Chatbot 과 상호작용 과정에서 다음에 어떤 질문을 할 수 있을지에 대한 제안을 사용자에게 출력해주는 시스템입니다.

이를 통해 사용자는 더욱 향상된 경험을 얻을 수 있고, 효과적으로 정보를 전달할 수 있을 것으로 생각되며, 특히 사용자가 Chatbot 에게 무엇을 물어봐야 할 지 모르는 상황이 있을 수 있는데 이때 효과적인 대안이 될 것으로 생각됩니다.

OpenAI Assistant Instruction ?

OpenAI Assistant 의 Instuction 은 특정한 답변 형식을 제어할 수 있는 지침으로 Chatbot이 어떻게 응답할 지 지정할 수 있는 지침이라 볼 수 있습니다.

해당 작업 과정에서는 기존 작성된 Instruction에서 추가로 두 가지 지침을 추가하였습니다.

  • JSON 형식 응답 : 모든 응답을 다음과 같이 JSON 형태로 출력할 수 있도록 합니다. {"response": "답변", "Suggested question": ["추천 질문1", "추천 질문2", "추천 질문3"]} 과 같은 형식으로 추천 질문의 경우 배열의 형태로 응답해주세요.
  • 추천 질문 지침 : 추천 질문의 경우 면접관이 "사용자"에 대해 궁금할 사항을 추천 질문으로 응답해주세요

백엔드 코드 구현

OpenAI 응답 (JSON 응답) 처리 및 MongoDB 저장 코드

아래와 같은 코드를 작성하여 사용자의 질문을 Assistant에서 처리 후 JSON 응답이 발생할 경우 response_data, suggested_questions 변수에 각각 응답과 추천 질문을 저장하여 사용자에게 전달할 수 있도록 했습니다.

하지만, AI 응답의 경우 명확하게 지정이 불가능하기에 가끔 JSON 응답이 출력되지 않는 경우가 발생할 수 있는데 해당 경우를 위해 JSON 응답이 발생하지 않을 경우 파싱 에러가 프린트 될 수 있도록하고, response_content 변수에 응답을 저장하고, suggested_questions 변수의 경우 빈 배열로 응답할 수 있도록 지정했습니다.

만약 빈배열이 아닌 기본 질문을 삽입하여 응답으로 전달할 경우 최초 추천 질문이 사용자에게 응답으로 전달되도록 지정할 수도 있습니다.

if last_message:
    try:
        response_data = last_message.content[0].text.value
        response_json = json.loads(response_data)  # OpenAI Assistant의 응답이 JSON 형식임을 가정
        
        response_content = response_json.get("response", "")
        suggested_questions = response_json.get("Suggested question", [])
    except (json.JSONDecodeError, KeyError, TypeError) as e:
        print(f"JSON parsing error: {e}")
        response_content = last_message.content[0].text.value
        suggested_questions = []

    data = {
        "time": datetime.datetime.now(datetime.UTC),
        "client_ip": client_ip,
        "question": user_message,
        "response": response_content,
        "suggested_questions": suggested_questions
    }

    try:
        mongo.db.responses.insert_one({
            "_time": data["time"],
            "client_ip": data['client_ip'],
            "user_message": data['question'],
            "assistant_message": data['response'],
            "suggested_questions": data['suggested_questions']
        })
    except Exception as e:
        print(f"Mongodb에 응답을 저장하는데 실패했습니다. {e}")
        return jsonify({"response": "데이터베이스에 응답을 저장하는데 실패했습니다. project5587@gmail.com 로 문의바랍니다."}), 500

    return jsonify({"response": response_content, "suggested_questions": suggested_questions, "thread_id": thread.id})

else:
    return jsonify({"response": "Assistant로부터 응답이 없습니다. 잠시후 다시 시도해주세요."}), 500

프론트엔드 코드 구현 (Chatbot Component)

아래와 같이 Chatbot Component 구현하였습니다.

import React, { useState, useRef, useEffect } from "react";
import styles from "../styles/Chatbot.module.css";
import axios from "axios";

function Chatbot() {
  const [isChatbotVisible, setIsChatbotVisible] = useState(false);
  const [input, setInput] = useState("");
  const [messages, setMessages] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [suggestedQuestions, setSuggestedQuestions] = useState([
    "이종욱에 대해 말해주세요.",
    "이종욱의 이력을 알려주세요.",
    "이종욱의 성격의 장단점을 알려주세요."
  ]);

  const messagesEndRef = useRef(null);
  const chatbotContainerRef = useRef(null);

  const handleChatbot = () => {
    setIsChatbotVisible(!isChatbotVisible);
  };

  const handleClickOutside = (event) => {
    if (chatbotContainerRef.current && !chatbotContainerRef.current.contains(event.target)) {
      setIsChatbotVisible(false);
    }
  };

  useEffect(() => {
    if (isChatbotVisible) {
      document.addEventListener("mousedown", handleClickOutside);
    } else {
      document.removeEventListener("mousedown", handleClickOutside);
    }
    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [isChatbotVisible]);

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  };

  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  const sendMessage = (question) => {
    const getChatbotResponse = async () => {
      const threadId = localStorage.getItem("thread_id");
      const requestData = {
        question: question || input
      };
      if (threadId) {
        requestData.thread_id = threadId;
      }

      try {
        const response = await axios.post(
          "https://api.jongwook.xyz/chat",
          requestData,
          {
            headers: {
              "Content-Type": "application/json",
            },
          }
        );

        const botResponse = response.data.response;
        const responseThreadId = response.data.thread_id;
        const newSuggestedQuestions = response.data.suggested_questions;

        // Save thread_id to localStorage
        if (responseThreadId) {
          localStorage.setItem("thread_id", responseThreadId);
        }

        setMessages((prevMessages) => [
          ...prevMessages,
          { text: botResponse, user: "bot" },
        ]);

        if (newSuggestedQuestions) {
          setSuggestedQuestions(newSuggestedQuestions);
        }
        else {
          setSuggestedQuestions(["이종욱에 대해 말해주세요.", "이종욱의 이력을 알려주세요.", "이종욱의 성격의 장단점을 알려주세요."]);
        }
      } catch (error) {
        console.error(error);
        setMessages((prevMessages) => [
          ...prevMessages,
          { text: error.response.data.response, user: "bot" },
        ]);
      } finally {
        setIsLoading(false);
      }
    };

    if ((question || input).trim()) {
      setMessages((prevMessages) => [
        ...prevMessages,
        { text: question || input, user: "me" },
      ]);
      setInput("");
      setIsLoading(true);
      setSuggestedQuestions([]);
      getChatbotResponse();
    }
  };

  return (
    <>
      <div className={styles.chatbot}>
        <button className={styles.chatbotBtn} onClick={handleChatbot}>
          <img
            src="https://freesvg.org/img/1538298822.png"
            alt="chatbot"
            className={styles.chatbotImg}
          />
        </button>
      </div>
      {isChatbotVisible && (
        <div className={styles.overlay} onClick={handleChatbot}>
          <div className={styles.chatbotContainer} ref={chatbotContainerRef} onClick={(e) => e.stopPropagation()}>
            <div className={styles.chatHeader}>AI Chatbot</div>
            <div className={styles.chatMessages}>
              {messages.map((message, index) => (
                <div
                  key={index}
                  className={`${styles.message} ${message.user === "me" ? styles.me : styles.bot}`}
                >
                  <p>{message.text}</p>
                </div>
              ))}
              {isLoading && (
                <div className={styles.loading}>
                  <p>Loading...</p>
                </div>
              )}
              <div ref={messagesEndRef} />
            </div>
            {suggestedQuestions.length > 0 && (
              <div className={styles.suggestedQuestions}>
                {suggestedQuestions.map((question, index) => (
                  <button
                    key={index}
                    className={styles.suggestedQuestionBtn}
                    onClick={() => sendMessage(question)}
                    disabled={isLoading}
                  >
                    {question}
                  </button>
                ))}
              </div>
            )}
            <div className={styles.chatInputContainer}>
              <input
                type="text"
                className={styles.chatInput}
                value={input}
                placeholder="Type a message..."
                onChange={(e) => setInput(e.target.value)}
                onKeyUp={(e) => e.key === "Enter" && !isLoading && sendMessage()}
                disabled={isLoading}
              />
              <button
                className={styles.sendButton}
                onClick={() => sendMessage()}
                disabled={isLoading}
              >
                Send
              </button>
            </div>
          </div>
        </div>
      )}
    </>
  );
}

export default Chatbot;

 

실제 구현

홈페이지 접근 후 우측 하단 Chatbot 버튼 클릭 시 다음과 같이 추천 질문 시스템이 포함된 인터페이스 출력

이후 추천 질문에 출력된 질문 클릭 시 아래와 같이 백엔드 서버로 질문이 전달되며, 해당 질문에 대한 추가적인 추천 질문이 인터페이스 내에 출력되는 것을 볼 수 있음