JaveScript/ExpressJS

ExpressJS와 ChatGPT 연동 (1)

알면 알 수록 재밌다! 2023. 12. 18. 19:01

 

저번 글에서는 facebook chatgpt인 wit.ai를 사용하다가 별로 인것 같아서 제거했다.

이번 글에서는 chatgpt를 연동해보고자 한다.

 

참고로 23년부터인가 무료 token이 만료되서 이제 api 호출을 하려면 카드 등록을 하고 5달러를 내야 테스트등 뭐를 할 수 있다.

그래서 mastercard, visa 등 되는 카드를 준비하길 바란다.

 

1편은 설정하기 및 입력값 넣기

2편은 출력된 코드 parsing하기

 

위처럼 글을 작성할 예정이므로 참고하시길 바란다.

 


 

1. 가입하기

 

 

로그인 - api 눌러서 소셜 로그인을 한다.

 


 

2.지불 방법 추가

 

최소 결제 금액은 5달러이고, 결제를 마치면 5달러가 충전되어있다.(수수료10%...)

 

3. playground 테스트

 

 

 

위처럼 훈련시킬 봇을 만들어서 훈련할 수도 있다.

테스트 용도로 해보자.

위에서 5달러 충전 안하면 이용 못하는 점 명심바란다.

 

 

 

 

나는 입력한 음식의 탄단지, 칼로리가 필요했다.

검색해보니 위처럼 string으로 값을 알려주는 줄 알았는데, openai.ChatCompletions.create 호출 반환값은 JSON 객체라고 한다. object 값을 deserialize한 값을 리턴한다.

저 햄버거, 콜라를 키를 가지고 있는 것의 값을 parsing해서 쓰면 chatgpt 연동 api가 되지 않을까 하는 것이 아이디어이다.

 

문제점이 3개가 생겼다.

 

첫번째는 우유 2개라고 입력한 경우에도 1개의 값만 string으로 알려줬다.

저 1개의 값을 내가 직접 *2 해야한다는 것을 깨달았다.

 

두번째는 약 200-300 처럼 나오는데, 이것도 200 이렇게 1개만 알려주는게 아니라 200-300으로 알려주니 이것도 변형을 시켜야하는 것이다.

즉, 다른 문장인 탄단지 에서도 이렇게 나올 수 있는 경우의 수가 생긴다는 것이다.

저 값을 바로 가져다 쓰려고 했는데, 값의 예외처리를 해야할 것이 추가되었다.

 

세번째는 값을 항상 리턴하는게 아니라는 점이다.

중량까지 입력해야하는 것인가라는 문제점이 생겼다.

이는 테스트를 해보며 prompt를 조정해 나가야하는 것 같다.

 

 

7번 검색했는데 아직도 0.01 달러라고 한다.

생각보다 인심이 후하다.

충분한 테스트를 할 수 있을것 같다.

 


 

4. expressjs에 연동하기

 

npm install openai

 

패키지를 설치한다.

 

# .env.local

OPENAI_API_KEY=mysecretkey

 

 

나는 .env.local 파일을 만들고 시크릿 키를 붙여넣었다.

참고로 위처럼 create 버튼을 만들고 시크릿 키를 만들어서 붙여넣으면 된다.

 

단순히 curl 호출을 해볼거라면

링크를 참고한다.

 

 

설명이 자세히 나와있어서 curl 호출하기 편하다

 

import OpenAI from 'openai';

const openaiInstance = new OpenAI({
  apiKey: process.env['OPENAI_API_KEY'],
});

export const openAI = async (foodSentence: string): Promise<string> => {
  let result = '';

  const stream = await openaiInstance.chat.completions.create({
    model: 'gpt-3.5-turbo',
    messages: [{ role: 'user', content: foodSentence }],
    stream: true,
  });

  for await (const chunk of stream) {
    const content = chunk.choices[0]?.delta?.content || '';
    result += content;
  }

  return result;
};

 

나는 위처럼 작성했다.

위 코드는 공식문서를 참고했는데, 

 

1) model

어떤 모델을 선택할건지는 제일 싼 gpt-3.5-turbo 를 했다.

 

2) role

 

"user" (사용자): 일반적으로 사용자가 하는 발언에 할당됩니다.
"assistant" (어시스턴트): 모델이 생성한 응답에 할당됩니다.
"customer" (고객): 고객 서비스 관련 대화에서 사용될 수 있습니다.
"agent" (에이전트): 지원이나 상담과 관련된 역할로 사용될 수 있습니다.
"speaker_1", "speaker_2", ... : 대화 중에 여러 참여자를 식별할 때 사용할 수 있는 일반적인 형태입니다.

 

role에 관해서는 개발자가 커스터마이징 할 수 있는 것 같다.

어떤 롤에서는 어떤 contents를 넣고 이런 식으로 말이다.

근데, 기본은 user이고 모델 학습을 시켰다면 assistant, chatbot의 경우 customer 등 이렇게 쓰는건가보다.

 

3) content

 

 

이건 우리가 chatgpt에 넣는 message를 말한다.

입력값을 넣는 파라미터이다.

 

4) stream

 

모델이 각각의 메시지에 대한 응답을 생성하는 즉시 사용할 수 있도록 하는 파라미터 옵션이다.

일반적인 요청 - 응답 api에서는 결과가 1번 반환되지만, 스트리밍 방식에서는 모델이 각 메세지에 대한 응답을 생성할 때마다 그 결과를 조금씩 반환한다.

이런 식으로 결과가 생성될 때마다 get을 계속 호출한다.

 

부분적으로 호출된것부터 리턴하는 방식을 말하며, 인스타그램을 처음에 키면 사진이 불러와진거부터 보여지는 화면을 본 적이 있을텐데, 그와 비슷한 기술인 것 같다.

 

5) chunk

 

{
  "id": "chatcmpl-XXXXX",
  "object": "chat.completion",
  "created": 11111111111,
  "model": "gpt-3.5-turbo",
  "usage": {
    "prompt_tokens": 1,
    "completion_tokens": 1,
    "total_tokens": 1
  },
  "choices": [
    {
      "message": {
        "role": "user",
        "content": "This is the model's response."
      },
      "finish_reason": "stop",
      "index": 0
    }
  ]
}

 

for await (const chunk of stream) {
    const content = chunk.choices[0]?.delta?.content || '';
    result += content;
}

 

chunk는 스트리밍 결과의 일부로서, 모델이 생성한 응답을 포함하게 된다.

각 chunk에서 첫 번째 선택의 내용 delta.content를 추출하고 이를 result에 추가한다.

result에는 전체 대화 결과가 쌓이게 된다.

즉, string로 구성된 chunk를 계속해서 추가해서 문장을 리턴하는 코드이다.

 

스트리밍된 결과는 비동기적으로 받아오게 되니까 순서가 뒤죽박죽일 테지만, 어차피 result라는 전체 문장이 중요한것이지 문장의 순서는 중요하지 않으므로 이런 코드를 제공한 것 같다.

 

결과물은 JSON 객체지만, 내가 필요한 데이터는 content라는 키의 value이므로

저 content 값을 parsing해야하는 것이 결론이다.

 

근데, choices가 배열이므로 저기를 제어문으로 돌려서 각각의 message의 content를 가져오는 것이 아닐까 싶다.

 


 

4. 입력값을 한문장으로 만들기

 

export const openAI = async (foodSentence: string): Promise<string> => {
  let result = '';

  const stream = await openaiInstance.chat.completions.create({
    model: 'gpt-3.5-turbo',
    messages: [{ role: 'user', content: foodSentence }],
    stream: true,
});

 

foodSentence에 값을 넣으려면 string으로 된 값을 넣어야한다.

 

import ERROR_CODE from '~/libs/exception/errorCode';
import ErrorResponse from '~/libs/exception/errorResponse';

interface FoodItem {
  food: string;
  quantity: number;
}

export const foodSentence = async (foods: FoodItem[]): Promise<string> => {
  const foodDescriptions: string[] = [];

  for (const food of foods) {
    const description = `${food.food} ${food.quantity}개의 탄수화물, 단백질, 지방과 그리고 칼로리를 알려줘`;
    foodDescriptions.push(description);
  }
  return foodDescriptions.join('\n');
};

 

그래서 나는 FoodItem이라는 음식 종류와 개수를 받고 싶어서 위처럼 작성했다.

음식 종류와 개수를 0개 입력하면 에러를 내뱉고,

제대로 입력했다면 그 결과 문장을 줄바꿈한 채로 1개의 문장으로 리턴해줄 것이다.

 

참고로 description이 message에 들어갈 입력값이다.

저기에 입력할 format을 작성했다.

 

import { foodSentence } from '~/libs/util/foodSentence';
import { openAI } from '~/libs/util/chatgpt';

// chatgpt 연동해서 foods의 탄단지, 칼로리 계산값 리턴
const foodArrayToString = await foodSentence(foods);
const openAIResult = await openAI(foodArrayToString); // 문장에서 parse 해야함

 

 

모듈화한 코드들을 api에서 불러와서 사용했다.

 

이제는 저 object 속 choices 배열의 content 값을 parsing해야하는데, 이는 2편에서 계속 작성하도록 하겠다.

 


 

# 문제 발생

 

typescript를 사용해 코드를 작성했을 경우 아래처럼 에러가 발생했다.

 

error TS2307: Cannot find module 'openai' or its corresponding type declarations.

 

 

javascript로 간단하게 테스트했을 경우

## index.mjs

import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: "my_secret_key",
});

async function main() {
  const chatCompletion = await openai.chat.completions.create({
    messages: [{ role: 'user', content: 'give me pasta menu' }],
    model: 'gpt-3.5-turbo',
  });
  console.log("chatCompletion : ", chatCompletion.choices[0].message)
}

main();

 

 

위처럼 모듈을 작성해 테스트했을때는  chatgpt가 response 값을 제대로 주었다.


 

typescript를 사용해 코드를 작성했을 경우

 

## openAI.ts

import OpenAI from 'openai';

export const openAI = async (foodSentence: string) => {
  const openaiInstance = new OpenAI({
    apiKey: process.env.OPENAI_API_KEY,
    organization: process.env.ORGANIZATION_INFO,
  });

  try {
    const response: any = await openaiInstance.chat.completions.create({
      model: 'gpt-3.5-turbo',
      messages: [{ role: 'user', content: foodSentence }],
    });
    const messagesContent = response.choices[0]?.message;

    return messagesContent;
  } catch (error) {
    if (error instanceof OpenAI.APIError) {
      console.error(error.status); // e.g. 401
      console.error(error.message); // e.g. The authentication token you passed was invalid...
      console.error(error.code); // e.g. 'invalid_api_key'
      console.error(error.type); // e.g. 'invalid_request_error'
    } else {
      // Non-API error
      console.log(error);
    }
    return null;
  }
};

 

typescript를 사용하는 프로젝트에서는 에러가 났다.

 

 

모듈을 찾지 못했거나 타입을 못읽는다고 에러를 내뱉는다.

openai 패키지 내에 typescript 설정이 잘 안되어 있는건지 버전의 오류인건지

수정을 해야할 것이 추가되었다.

 


해본 것

 

  • 4.24, 4.22, 3.3.0 다운그레이드해서 테스트
  • organization parameter가 선택값이 아닐수도 있다는 생각에 추가해봤지만 이도 해결방안이 아니였음
  • tsconfig.json 에 moduleResolution: node 추가했지만 똑같음

 

javascript로는 테스트가 되었는데 typescript로 된 코드에서 실행이 안된다?
컴파일이 문제이지 않을까라고 생각이 들었다.

 

 


 

임시해결방법

docker build할때 openai 패키지가 ts-node 컴파일하면서 에러가 나서 패키지가 설치가 안된것 같다.

패키지 설치를 직접 해준다거나

도커 컨테이너 속으로 들어가서 패키지를 설치하니

정상적으로 서버가 돌아가기 시작한다.

 


세팅 후기

 

openai 패키지와 내 ts-config 설정 충돌인건지, 노드 패키지의 문제인건지는 아직 파악을 못하고 임시적으로만 작동되게 했다.

issue에 올려서 문의를 해봐야할 것 같다.

상당히 까다로운 패키지인것 같다.

파이썬으로 설치했을때는 엄청 쉬웠는데... ㅠㅠ