☁️ Tencent Cloud/☁️ Hands-on Tencent cloud

[Tencent Cloud] SCF + JANDI ChatGPT 챗봇 만들기

just in here

 

 

출처 : https://chat.openai.com

ChatGPT Chatbot

 Slack에 ChatGPT를 연동하여 간단한 챗봇을 구현하는 작업은 이미 너무 유명하여 구글링만 해봐도 수 십개의 관련 블로그글이 검색된다. 가장 기본적인 방식은 AWS Lambda를 활용한 구현인데, 이번에 우리는 람다대신 Tencent cloud SCF로 메신저에 ChatGPT 챗봇을 만들어보기로 한다.

 

JANDI

출처 : https://www.jandi.com/landing/kr

 

 이번에는 슬랙 대신 JANDI라는 메신저를 활용할 예정이다. 사내에서 사용하는 메신저를 겸한 협업툴인데

 

 메신저 내에서 이미 '잔디커넥트'라는 자체기능을 지원하고 있다. 기본적인 웹훅 기능뿐 아니라, 아틀라시안 툴이나 깃허브 등을 연동하여 커밋 변경사항이 생기면 메신저를 통해 알람을 받는 것이 가능하다. 슬랙의 경우 인터랙티브 응답 시, 슬랙서버로부터 요청을 받은 후 3초 안에 응답하지 않으면 타임아웃에러가 나도록 설정되어 있는데, 슬랙봇을 호출하고 chatGPT 질의를 거쳐 메시지를 다시 수신하는 과정은 당연히 3초를 초과하기 때문에 추가작업이 필요하다.

 

 따라서 기존 슬랙과 람다를 통한 챗봇구현 방식은 중간에 SQS 등의 큐 서비스를 배치하여 비동기적으로 3초 룰에 대처하도록 설계하곤 하는데 잔디는 그런 타임아웃 제한이 없기 때문에 잔디 사용 시 아키텍처가 좀 더 간단해진다는 장점이 있다.

 

 또한 잔디 공식 블로그에서는 업무 자동화 툴인 zapier와 ChatGPT를 연동하는 방법을 자세히 소개하고 있는데(https://blog.jandi.com/ko/2023/03/15/how-to-chatgpt-like-a-pro/) 이 글에서는 zapier대신 Tencent cloud SCF와Tencent cloud API Gateway를 사용하는 방법을 소개한다.

 

아키텍처

 

대략적인 아키텍처는 아래와 같다.

잔디에서 '/' 기호를 붙여 챗봇을 호출하고 질문을 던지는 이벤트를 발생시키면 잔디의 웹훅으로 Tencent cloud API Gateway를 호출하고 해당 API Gateway를 트리거로 하는 SCF 코드가 실행되면 (SCF에는 ChatGPT에 질문을 던지고 대답을 리턴하는 코드를 구현한다.)  SCF는 리턴 값(챗gpt의 응답)을 잔디 수신 웹훅 URL로 전달하면 채팅창에 응답이 뿌려지는 과정이다. 

 

JANDI 발신 Webhook 설정

 

 JANDI 메신저에서 잔디 커넥트 부분 설정을 클릭하면 Webhook 발신 / Webhook 수신 두 개의 메뉴가 있다. Webhook 발신을 클릭하면 간단한 파라미터를 입력할 수 있고 챗봇이 생성될 토픽방을 선택하면 시작 키워드를 입력할 수 있다. 여기서 입력한 키워드 앞에 '/'기호를 붙이면 챗봇이 호출되는 방식이다.

 

 일반적으로 /chatgpt라는 호출 키워드를 많이 사용하지만 사내 직원들의 손쉬운 사용을 위해 /말동무라는 한글 키워드로 설정하였다. 마지막에 URL을 입력하는 부분이 있는데 여기에 뒤에서 생성할 API Gateway의 엔드포인트를 입력하면 된다.

 

다음으로 Webhook 수신 메뉴를 클릭해 보면 Webhook URL이 자동생성되는데 이 URL을 복사해 둔다. 나중에 SCF 코드 내에서 사용하게 된다.

SCF 함수 생성

순서상으로는 API부터 만들고 SCF 코드를 작성해야 하지만 API 생성 시 SCF를 트리거로 하려면 사전에 SCF가 생성되어 있어야 하기 때문에 미리 기본 SCF를 생성하는 작업을 한다.

 

Tencent cloud Serverless Cloud Function 콘솔에서 Function Service 항목에서 Create를 클릭한다.

 

 

Create from scratch를 선택한 뒤 Function type : Event-triggered function으로 설정하고 나머지 항목은 적절히 잘 입력한다. 입력이 완료되고 Complete를 누르면 SCF 생성완료.


API Gateway

다음은 잔디의 Webhook을 호출받을 API를 만들 차례다. Tencent cloud API Gateway 콘솔에 접속해서

 

Service 항목을 클릭한 뒤 API Gateway를 생성한다.

 

 

생성한 API Gateway를 클릭하고 Related APIs 항목에서 Create를 눌러 API 생성 메뉴를 띄운다.

 

 

Frontend type은 HTTP&HTTPS , Request method는 POST로 설정한다. path는 API엔드포인트 도메인 뒤에 붙을 경로를 의미하는데 적당히 네이밍 하면 된다. 여기서는 간단하게 JANDI2로 설정하였다.

 

다음 단계에서 Backend type은 SCF로 선택한 뒤 생성한 SCF함수를 선택한다. 이때 Function type은 Event-triggered function으로 한다.

 

마지막 단계에서 리턴 타입을 JSON으로 하고 API 생성을 완료한다.

 

 생성완료된 API의 정보를 확인하면 public domian API 엔드포인트를 얻을 수 있다. API호출 URL은 public domain + Access path 가 된다. 이미 본인이 보유하고 있는 다른 도메인을 바인딩해서 사용해도 되지만 테스트 단계이므로 일단 제공된 URL을 그대로 사용하기로 한다. 이 URL을 아까 전 JANDI 발신 Webhook 설정에서 URL을 입력하는 부분에 입력하면 된다.

OpenAI API Key 발급

 

이제 SCF 코드를 작성하기 전에 chatGPT API를 사용하기 위한 API Key를 발급받아야 한다.
OpenAI API Key 발급 URL (https://platform.openai.com/api-keys)로 이동해서 Secret Key를 생성하면 된다.

 

SCF 코드 작성


SCF 코드는(Nodejs기준) 총 3 부분으로 구성된다. 잔디로부터 메시지를 수신하여 chatGPT서버로 보낼 엔드포인트와 응답 값을 되돌려줄 잔디 측 수신처의 메타데이터는 모두 해당 OpenAI, JANDI의 공식 문서를 참고하여 작성하였다.

const express = require('express');
const axios = require('axios');
const serverless = require('serverless-http');
const app = express();
app.use(express.json());



app.post('/JANDI2', async (req, res) => {
  try {
    const userMessage = req.body.text; 
    const openaiResponse = await axios.post(
      'https://api.openai.com/v1/chat/completions',
      {
        model: 'gpt-3.5-turbo',
        messages: messages,
      },
      {
        headers: {
          'Authorization': `Bearer <OpenAI에서 발급받은 키 입력>`,
        },
      }
    );

POST요청을 통해 전달받은 메시지를 API Key와 함께 ChatGPT에게 전달한다.

    const chatGptMessage = openaiResponse.data.choices[0].message.content;

ChatGPT의 응답을 받아서

    await axios.post('<JANDI Webhook 수신 URL 입력>', {
      body: chatGptMessage,
    }, {
      headers: { 'Accept': 'application/vnd.tosslab.jandi-v2+json', 'Content-Type': 'application/json' },
    });


  } catch (error) {
    console.error('Error:', error);
    res.status(500).send({ error: 'Internal Server Error' });
  }

});

module.exports.main_handler = serverless(app);

 

잔디 Webhook 수신 URL로 던진다. 이때 성공(200) 케이스의 메시지의 경우 send 하면 매 번 챗봇을 호출할 때마다 잔디 메시지 창에 그대로 노출되므로 제외한다.

 

결과물 (...🤔)

/말동무를 사내 채팅방에서 호출하여 질문을 던져 본다.

 

정상적으로 응답이 오가는 걸 확인할 수 있지만 만족스럽지 않다.

 

가만 살펴보면 바로 전에 나눈 대화의 내용도 기억하지 못하고 있다. 그도 그럴 것이 SCF에 작성한 코드를 보면 단일 대화 이벤트에 대한 처리 이외에 이전 대화 히스토리를 유지하고 있는 부분이 빠져있다. 따라서 유저가 챗봇에 질문을 던질 때마다, 메모리 영역에서 이 대화를 히스토리에 추가하고, 질문을 할 때 이 히스토리를 ChatGPT에 통째로 전달하고 ChatGPT의 응답도 히스토리에 추가하여 전달한다. 예를 들면 이런 식이다.

유저 : 내 이름은 철수야
챗봇 : 네 철수님.

 

기존에는 위의 대화가 끝나고 새로운 질문을 던지면 독립적인 새 질문으로 전달되었지만, 이제는 새 질문을 입력하면 아래의 히스토리가 chatgpt에게 전해진다.

유저 : 내 이름은 철수야
챗봇 : 네 철수님.
유저 : 내 이름이 뭐랬지?

 

이전 대화 내용까지 다음 대화내용으로 전달하여 대화의 맥락을 기억하도록 전체 코드를 개선한다.

const express = require('express');
const axios = require('axios');
const serverless = require('serverless-http');
const app = express();
app.use(express.json());

let conv = {}; //대화 히스토리 저장


app.post('/JANDI2', async (req, res) => {
  try {
    const userMessage = req.body.text; 
    const uniqueID = req.body.senderId; // 대화 히스토리 식별자

    // 대화 히스토리 리콜
    const messages = conv[uniqueID] || [];

    messages.push({ role: 'user', content: userMessage });

    const openaiResponse = await axios.post(
      'https://api.openai.com/v1/chat/completions',
      {
        model: 'gpt-3.5-turbo',
        messages: messages,
      },
      {
        headers: {
          'Authorization': `Bearer <OpenAI 키 입력>`,
        },
      }
    );

    const chatGptMessage = openaiResponse.data.choices[0].message.content;

    // 대화 이력 업데이트
    messages.push({ role: 'assistant', content: chatGptMessage });
    conv[uniqueID] = messages;


    await axios.post('<잔디 Webhook 수신 URL 입력>', {
      body: chatGptMessage,
    }, {
      headers: { 'Accept': 'application/vnd.tosslab.jandi-v2+json', 'Content-Type': 'application/json' },
    });


  } catch (error) {
    console.error('Error:', error);
    res.status(500).send({ error: 'Internal Server Error' });
  }

});

module.exports.main_handler = serverless(app);

 

개선된 말동무의 지능

다시 챗봇을 호출하여 말을 걸어보면

 

개선된 지능을 확인할 수 있다. 

 

마치며

 ChatGPT와 서버리스를(Tencent Cloud SCF) 활용한 기본적인 챗봇을 구현해 보았다. 기존에 람다를 사용하던 사용자들도 별도의 학습 없이 함수를 생성할 수 있도록 콘솔이 잘 구성되어 있다.

 현재 JANDI에서는 일반회원 권한으로는 해당 기능을 적용하더라도 특정 토픽방에서만 사용이 가능하다.  '나와의 대화' 같은 공간에서 사용하기 위해서는 기업형 구독과 관리자 권한이 필요하니 참고한다.

 



참고사항

정상적으로 구현을 하더라도 로그에 429 에러가 찍히면서 응답이 오지 않을 수 있다. 이럴 때는 OpenAI의 API 사용을 위한 크레딧이 충전되지 않아서 그런 경우이니 사이트에서 금액을 충전해야 한다. 

 

충전 후에는 충전 이전에 발급받았던 API Key는 유효하지 않으니 새로 Key를 발급받아서 SCF 함수에 적용시켜야 한다.