6/5 openai API를 이용하여 text 생성하기

낄낄박사·2024년 6월 5일
post-thumbnail

openai API

원래 없던 계획이긴 한데, 날씨데이터를 이용해서 openai로 날씨에 얘기해주는 text를 생성해보기로 함.

각각의 캐스터 색깔에 맞는 말투로 텍스트를 정적으로 만들어 놓고, 동적으로 날씨데이터를 받아서 텍스트 사이사이에 넣어주려고 했는데, 그러자니 늘 똑같은 말투로 날씨를 알려주면 재미가 없을 것 같아서 openai를 써보면 좋겠다는 생각이 들었음.

한번도 써보지 않았지만 별로 어려울 것 없을거라 생각하고 시작..!

사용방법은 그렇게 어렵지 않음. docs 보고 그대로 하면 된다.
opneai docs(Node.js)

  1. openai 계정이 없다면 먼저 가입을 하고, API Key Page에서 secretKey를 생성해준다.
  2. Node가 설치 되어있지 않다면 설치하고, openai를 설치해준다.
npm install --save openai
# or
yarn add openai
  1. 프로젝트에 환경변수 셋팅해주기
export OPENAI_API_KEY='your-api-key-here'
  1. api 요청 API reference
  • 요청을 보낼 때는 "https://api.openai.com/v1/chat/completions"이 경로로 "POST"로 보내야함!
  • 반드시 포함되어야하는 항목은 messages와 model! 이외 옵셔널한 내용은 레퍼런스를 참조하면 됨.

response는 아래와 같은 형태로 받는다.

{
  "id": "chatcmpl-123",
  "object": "chat.completion",
  "created": 1677652288,
  "model": "gpt-3.5-turbo-0125",
  "system_fingerprint": "fp_44709d6fcb",
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": "\n\nHello there, how may I assist you today?",
    },
    "logprobs": null,
    "finish_reason": "stop"
  }],
  "usage": {
    "prompt_tokens": 9,
    "completion_tokens": 12,
    "total_tokens": 21
  }
}

답변을 추출하려면 다음을 사용해서 추출하면됨.

completion.choices[0].message.content
  1. 데이터 페칭 로직 구성
  • next api route를 사용할 것이므로 api route를 먼저 만들어준다.
    caster정보와 날씨 정보를 받아서 텍스트를 생성해야 하므로 body로 전달 받도록 함.
  • 데이터 검증 후 실제 openai api 요청 함수 호출
// app>api>weather-message>route.ts

import { generateWeatherMessage } from "@/app/service/openai";
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  const { caster, weather } = await req.json();

  if (!caster || !weather) {
    return NextResponse.json(
      { error: "Caster and weatherData are required" },
      { status: 400 }
    );
  }

  try {
    const res = await generateWeatherMessage(caster, weather);
    return NextResponse.json(res);
  } catch (error) {
    console.error("Error generating text:", error);
    return NextResponse.json(
      { error: "Error generating text" },
      { status: 500 }
    );
  }
}
  1. openai api 요청부분
onst response = await fetch("https://api.openai.com/v1/chat/completions", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        model: "gpt-4o",
        messages: [systemMessage, userMessage],
        max_tokens: 150,
      }),
    });
  • POST요청 /model / messgaes 포함!

그래서 전체 코드는 아래와 같이 구성

//src>app>service>openai.ts

export type WeatherData = {
  description: string;
  temp: number;
  temp_max: number;
  temp_min: number;
  precipitation: number | null;
  wind: number;
  snow: number | null;
  humidity: number;
  sunrise: number;
  sunset: number;
  icon: string;
};

export type Caster =
  | "이장님"
  | "할머니"
  | "엄마"
  | "여자캐스터"
  | "남자캐스터"
  | "KPOP매니아"
  | "먹방유튜버";

const casterDescriptions: Record<Caster, string> = {
  이장님:
    "날씨를 비유적으로 돌려서 설명하는 불만이 많고 유머러스한 충청도 시골 이장님",
  할머니:
    "나이 많고 무심한듯한 전라도 사투리를 사용하며 잔소리하는 욕쟁이 할머니",
  엄마: "따뜻하게 돌봐주는 엄마",
  여자캐스터: "밝고 활기찬 기상캐스터",
  남자캐스터: "자신감있는 기상캐스터",
  KPOP매니아: "한국 노래를 좋아하고 모르는 노래가 없는 한국 노래 매니아",
  먹방유튜버: "유머러스하고 날씨에 딱 어울리는 음식 추천을 잘하는 먹방유튜버",
};

type Message = {
  caster: Caster;
  message: string;
};

export async function generateWeatherMessage(
  caster: Caster,
  weatherData: WeatherData
): Promise<Message> {
  const systemMessage = {
    role: "system",
    content: `너는 ${casterDescriptions[caster]}(으)로 행동할거야. 캐릭터 성격에 부합하는 말만 해줘. `,
  };

  const userMessage = {
    role: "user",
    content: `
    날씨 설명: ${weatherData.description}
    현재 기온: ${Math.floor(weatherData.temp)}도
    최고 기온: ${Math.floor(weatherData.temp_max)}도
    최저 기온: ${Math.floor(weatherData.temp_min)}도
    강수량: ${weatherData.precipitation}mm
    바람: ${weatherData.wind}m/s
    습도: ${weatherData.humidity}%
    캐릭터가 오늘 날씨에 대해 얘기해줘. 답변은 3-4문장으로 해줘. 날씨정보에 따라 캐릭터의 성격에 맞는 날씨 설명을 해줘. 단, '바람', '습도'라는 직접적인 단어사용은 하지마. `,
  };

  const apiKey = process.env.OPENAI_API_KEY;
  if (!apiKey) {
    throw new Error("API key is missing");
  }

  try {
    const response = await fetch("https://api.openai.com/v1/chat/completions", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        model: "gpt-4o",
        messages: [systemMessage, userMessage],
        max_tokens: 150,
      }),
    });

    if (!response.ok) {
      throw new Error(`Error ${response.status}`);
    }

    const data = await response.json();
    const message = {
      caster,
      message: data.choices[0].message.content,
    };

    return message;
  } catch (error) {
    console.error("Error generating text:", error);
    throw error;
  }
}

이유는 모르겠지만 4o버전이 대답을 좀더 내가 원하는 방향으로 해주는 것 같아서 4o를 사용하고 있지만 3.5 버전으로도 충분히 좋았음.

근데

서버에러가 계속 나서 이틀을 고민함..

처음에는 환경변수에서 api를 못 읽어오는 문제가 있었음. 그래서 dotenv 패키지도 설치해봤는데, 아무 소용없어서 다시 돌려놓고, openai 관련된 로직을 다 살펴봤는데도 문제가 없었음.

그래서 oepnai 대시보드를 확인해보니 usage:cost라는게 있음.
아무래도 유료인가하여 Billing에 들어가서 5달러만 내고 credit을 추가함.

그래도 안되는 것 같았는데, 어느 순간 갑자기 response가 찍히기 시작함.ㅋㅎㅋㅎ.. 페이를 하고 약간의 시간이 지나야 하나봄.

크레딧이 없어서 에러가 났던것 같은데 확인해보고자, 다른 구글 계정으로 로그인 후 프로젝트 apiKey 생성하고, 크레딧 없이 다시 api 요청 해봄.

안 됨!

크레딧이 필요함.

어쨌든.. response받아서 UI에서 보여주면,

import React, { useEffect, useState } from "react";
import { useCaster } from "../context/CasterContext";
import { useWeather } from "../context/WeatherContext";

export default function WeatherMessage() {
  const [message, setMessage] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const { weather } = useWeather();
  const { caster } = useCaster();

  useEffect(() => {
    async function fetchWeatherMessage() {
      if (caster && weather) {
        try {
          setIsLoading(true);
          const response = await fetch("/api/weather-message", {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify({ caster, weather }),
          });
          const data = await response.json();
          setMessage(data.message);
          setIsLoading(false);
        } catch (error) {
          console.error("Error fetching weather message:", error);
        }
      }
    }

    fetchWeatherMessage();
  }, [caster, weather]);

  return (
    <>
      <div className="flex w-96 items-center justify-center bg-indigo-100 rounded-3xl p-6 h-72">
        {isLoading && <p>Generating message...</p>}
        {!isLoading && message && <p>{message}</p>}
      </div>
    </>
  );
}

짠!

0개의 댓글