본문 바로가기
IT_알토르/기술 및 코드과제

11. OpenAI Assistant, API 엔드포인트 sendMessage 요청

by jys275 2025. 5. 1.

openAI Assistant가 무엇인지 

간단하게 OpenAI에게 역할과 성격을 부여해서 "특정 일을 하는 직원처럼" 일하게 만드는 기능 : 예를 들어, "너는 이제부터 이력서 전문가야"라고 역할을 고정시키는 것임
 
즉, 기존 ChatGPT 모델을 하나의 "역할을 가진 AI"로 정의하고, 지속적인 대화(thread)를 저장하며, 파일 업로드, 코드 실행 등까지 가능한 GPT 확장 기능 : 단순히 prompt만 보내던 방식에서 벗어나 GPT를 "챗봇 인격 하나"처럼 만들 수 있는 고급 API
 
thread_id
Assistant와 사용자 간의 "하나의 대화방 ID"라고 할 수 있음 : 즉, thread_id는 특정 대화 세션을 식별하는 고유 ID : Assistant와의 대화는 thread를 기준으로 저장 : 하나의 사용자와 Assistant 간 대화 흐름은 하나의 thread에 기록 : thread_id를 생성하면 이후의 메시지는 해당 thread에 추가

즉, thread_id 없이는 Assistant에게 "지속적인 대화"를 시킬 수 없음


assistant에 인스트럭션(설명, 챗봇의 역할), 본인 샘플 이력서 업로드

OpenAI Assistant의 행동 지침을 결정짓는 설정인 System instructions 입력 : Name 아래가 thread_id : 업로드한 File을 Assistant가 읽고, 그 안에서 적절한 정보를 찾아 답변에 활용할 수 있도록 File Search 활성화 후, 샘플 이력서 첨부
 
 


 


API 엔드포인트에 sendMessage에 요청을 했을때, 메세지 내용이 Assistant에 요청 및 응답을 받아오도록

백엔드(app.py) 변경

@app.route('/sendMessage', methods=['POST'])
def send_message():
    try:
        # 메시지와 스레드 ID 받기 (대화 맥락 유지)
        user_message = request.json.get('message')
        thread_id = request.json.get('thread_id')

        # 스레드 생성 또는 기존 스레드 사용
        if not thread_id:
            # 새 스레드 생성 API 호출
            thread_response = requests.post(...)
            thread_id = thread_data.get("id")

        # 메시지 추가 → 실행 → 완료 대기 → 응답 가져오기 단계로 분리
        # 메시지 추가
        message_response = requests.post(...)

        # 어시스턴트 실행
        run_response = requests.post(...)

        # 완료 대기 (폴링)
        while attempts < max_attempts:
        # 상태 확인...

        # 완료되면 응답 가져오기
        if status == "completed":
            messages_response = requests.get(...)

            # 메타데이터 제거 및 응답 반환
            cleaned_content = clean_assistant_response(assistant_content)
            return jsonify({"response": cleaned_content, "thread_id": thread_id})

        # 각 단계별 오류 처리 추가
    except Exception:
# 예외 처리
  • API 변경: Chat Completions → Assistants API (v2)
  • 대화 맥락 관리: 스레드 ID를 통한 대화 이력 유지 추가 
  • 단계 세분화: 4단계 처리 과정으로 분리 (메시지 추가 → 실행 → 대기 → 응답 가져오기)
  • 오류 처리 강화: 각 단계별 오류 확인 및 예외 처리
  • 대기 방식: 폴링 방식으로 완료 대기, 최대 시간 제한

 
프론트엔드(Chat.jsx) 변경

  const [inputMessage, setInputMessage] = useState('');
  const [threadId, setThreadId] = useState(null);
  const [isTyping, setIsTyping] = useState(false);
  const messagesEndRef = useRef(null);
  const chatContainerRef = useRef(null);
  const typingSpeed = 10; // 타이핑 속도 (ms)를 10ms로 변경하여 더 빠르게 설정

  // 새 메시지가 추가될 때 채팅창 내부만 스크롤
  useEffect(() => {
    if (messagesEndRef.current && chatContainerRef.current) {
      // 채팅 컨테이너 내부에서만 스크롤 수행
      chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
    }
  }, [messages]);

  // 타이핑 효과 구현
  useEffect(() => {
    const typingMessage = messages.find(msg => msg.isTyping);
    
    if (typingMessage) {
      if (typingMessage.displayText.length < typingMessage.text.length) {
        const timer = setTimeout(() => {
          setMessages(prevMessages => 
            prevMessages.map(msg => 
              msg.id === typingMessage.id 
                ? { 
                    ...msg, 
                    displayText: msg.text.substring(0, msg.displayText.length + 1) 
                  } 
                : msg
            )
          );
        }, typingSpeed);
        
        return () => clearTimeout(timer);
      } else {
        // 타이핑 완료
        setMessages(prevMessages => 
          prevMessages.map(msg => 
            msg.id === typingMessage.id 
              ? { ...msg, isTyping: false } 
              : msg
          )
        );
        setIsTyping(false);
      }
    }
  }, [messages]);

  // 채팅 메시지 전송 함수
  const handleSendMessage = async (e) => {
    e.preventDefault();
    if (inputMessage.trim() === '' || isTyping) return;

    // 사용자 메시지 추가
    const newUserMessage = {
      id: messages.length + 1,
      text: inputMessage,
      displayText: inputMessage,
      isUser: true
    };
    setMessages([...messages, newUserMessage]);
    setInputMessage('');

    try {
      // 로딩 메시지 추가
      setMessages(prev => [
        ...prev,
        {
          id: prev.length + 1,
          text: '',
          displayText: '',
          isUser: false,
          isLoading: true
        }
      ]);

      // Flask 서버로 POST 요청 (JSON 방식)
      const response = await axios.post('http://localhost:5001/sendMessage', {
        message: inputMessage,
        thread_id: threadId
      });

      // 로딩 메시지 제거
      setMessages(prev => prev.filter(msg => !msg.isLoading));

      // 응답에서 스레드 ID 업데이트
      if (response.data.thread_id) {
        setThreadId(response.data.thread_id);
      }

      // AI 응답 추출 및 추가
      if (response.data.response) {
        setIsTyping(true);
        setMessages(prev => [
          ...prev,
          {
            id: prev.length + 1,
            text: response.data.response,
            displayText: '', // 처음에는 빈 문자열로 시작
            isUser: false,
            isTyping: true
          }
        ]);
      } else if (response.data.error) {
        // 오류 메시지 처리
        setMessages(prev => [
          ...prev,
          {
            id: prev.length + 1,
            text: `오류: ${response.data.error}`,
            displayText: `오류: ${response.data.error}`,
            isUser: false
          }
        ]);
      }
    } catch (error) {
      // 로딩 메시지 제거
      setMessages(prev => prev.filter(msg => !msg.isLoading));
      
      // 오류 메시지 추가
      setMessages(prev => [
        ...prev,
        {
          id: prev.length + 1,
          text: 'AI 서버와 통신 중 오류가 발생했습니다.',
          displayText: 'AI 서버와 통신 중 오류가 발생했습니다.',
          isUser: false
        }
      ]);
      console.error('에러 상세 내용:', error);
    }
  };

  function renderMessageText(message) {
    const { text, displayText, isLoading } = message;
    
    // 로딩 메시지인 경우
    if (isLoading) {
      return (
        <div className="flex items-center justify-center">
          <div className="dots-container">
            <div className="dot"></div>
            <div className="dot"></div>
            <div className="dot"></div>
          </div>
        </div>
      );
    }
    
    // 코드 블록 감지
    const codeBlockMatch = displayText.match(/```(.*?)\\n([\\s\\S]*?)```/);
    if (codeBlockMatch) {
      const language = codeBlockMatch[1];
      const code = codeBlockMatch[2].replace(/\\n/g, '\n'); // 줄바꿈 처리
      return (
        <pre style={{background: '#222', color: '#fff', padding: 10, borderRadius: 8, overflowX: 'auto'}}>
          <code>
            {code}
          </code>
        </pre>
      );
    }
    // 일반 텍스트
    return <span>{displayText}</span>;
  }
  • 상태 관리 확장: threadId 상태 추가로 대화 맥락 유지
  • UX 개선: 로딩 상태 표시로 사용자 피드백 강화
  • 응답 구조 변경: 서버 응답 구조에 맞게 처리 방식 수정
  • 기존: response.data.choices[0].message.content
  • 변경: response.data.response
  • 오류 처리 개선: 로딩 메시지 제거 및 다양한 오류 응답 처리
  • 스타일 개선: 로딩 애니메이션 추가 및 메시지 렌더링 기능 강화
  • 타이핑 효과 구현 : isTyping 상태 추가로 응답이 한 글자씩 나타나는 효과 구현

 
실제 사용