🎉

OpenAIのChatGPT API (gpt-3.5-turbo )を使ってみた

2023/03/02に公開
1

こんにちはstdと申します。

OpenAIのChatGPT API(gpt-3.5-turbo)が使えるようになった旨のメールが届きましたので試しに実装してみましたので実装方法を書いていきます。

https://platform.openai.com/docs/guides/chat/introduction

先日作った個人開発のAI Senseiに組み込んでみます。

できたもの

実装

このコードを参考にしました。
https://github.com/Nutlope/twitterbio

事前にOpenAIのAPI KEYを.envに保存しておきます。

OPENAI_API_KEY=YOUR_API_KEY

型を作成

text-davinci-003ではpayloadでprompt: stringを渡していましたが、
gpt-3.5-turboではこのファイルで作っているmessages: Message[]を渡します。

//src/types/types.ts

export type Role = "system" | "user" | "assistant";

export type Message = {
  role: Role;
  content: string;
};

export interface OpenAIStreamPayload {
  model: string;
  messages: Message[];
  temperature: number;
  top_p: number;
  frequency_penalty: number;
  presence_penalty: number;
  max_tokens: number;
  stream: boolean;
  n: number;
}

export interface ChatResponseData {
  id: string;
  object: string;
  created: string;
  model: string;
  choices: { delta: Partial<Message> }[];
}

ChatGPT APIを呼び出す関数を作成

ほぼ参考のコードと同じですが
fetchのURLをChatGPT用に変更しているのと
textの取り方を
const text = responseData.choices[0].delta.content;
に変更しています。

//src/utils/OpenAIStream.ts

import {
  createParser,
  ParsedEvent,
  ReconnectInterval,
} from "eventsource-parser";
import { OpenAIStreamPayload, ChatResponseData } from "@/types/types";

export async function OpenAIChatStream(payload: OpenAIStreamPayload) {
  const encoder = new TextEncoder();
  const decoder = new TextDecoder();

  let counter = 0;

  const res = await fetch("https://api.openai.com/v1/chat/completions", {
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.OPENAI_API_KEY ?? ""}`,
    },
    method: "POST",
    body: JSON.stringify(payload),
  });

  if (!res.ok) {
    throw new Error(res.statusText);
  }

  const stream = new ReadableStream({
    async start(controller) {
      // callback
      function onParse(event: ParsedEvent | ReconnectInterval) {
        if (event.type === "event") {
          const data = event.data;
          // https://platform.openai.com/docs/api-reference/chat/create
          if (data === "[DONE]") {
            controller.close();
            return;
          }
          try {
            const json = JSON.parse(data);
            const responseData = json as ChatResponseData;
            const text = responseData.choices[0].delta.content;
            if (!text || (counter < 2 && (text.match(/\n/) || []).length)) {
              // this is a prefix character (i.e., "\n\n"), do nothing
              return;
            }
            const queue = encoder.encode(text);
            controller.enqueue(queue);
            counter++;
          } catch (e) {
            // maybe parse error
            controller.error(e);
          }
        }
      }

      // stream response (SSE) from OpenAI may be fragmented into multiple chunks
      // this ensures we properly read chunks and invoke an event for each SSE event stream
      const parser = createParser(onParse);
      // https://web.dev/streams/#asynchronous-iteration
      for await (const chunk of res.body as any) {
        parser.feed(decoder.decode(chunk));
      }
    },
  });

  return stream;
}

Edge Functions作成

apiディレクトリにopenai.tsを作成。
このファイルで先ほどのOpenAIStreamを呼び出してストリームでレスポンスを返していきます。
messagesは先頭にシステムのコンテンツを挿入します。
これにより全体のChatGPTの回答内容に指示できるようです。

//src/pages/api/openai.ts

import { OpenAIChatStream } from "@/utils/OpenAIStream";
import { Message, OpenAIStreamPayload } from "@/types/types";

const systemMessage: Message = {
  role: "system",
  content:
    "You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
};

if (!process.env.OPENAI_API_KEY) {
  throw new Error("Missing env var from OpenAI");
}

export const config = {
  runtime: "edge",
};

const handler = async (req: Request): Promise<Response> => {
  try {
    const { messages } = (await req.json()) as {
      messages?: Message[];
    };

    if (!messages) {
      return new Response("No prompt in the request", { status: 400 });
    }

    const payload: OpenAIStreamPayload = {
      model: "gpt-3.5-turbo",
      messages: [systemMessage, ...messages],
      temperature: 0.7,
      top_p: 1,
      frequency_penalty: 0,
      presence_penalty: 0,
      max_tokens: 1000,
      stream: true,
      n: 1,
    };

    const stream = await OpenAIChatStream(payload);
    return new Response(stream);
  } catch (error: any) {
    console.error(error);
    return new Response("Something went wrong", { status: 500 });
  }
};

export default handler;

hook作成

Edge Functions呼び出し用のhookを作ります。
レスポンスがストリームで流れてくるので以前の文字列の後ろに結合して保存していきます。

//src/hooks/useOpenAI.ts

import { showNotification } from "@mantine/notifications";
import { Message } from "@/types/types";

const useOpenAI = () => {
  const fetchStream = async (
    messages: Message[],
    setLoading: (value: boolean) => void,
    setReply: (value: string, isNew?: boolean) => void,
  ) => {
    setLoading(true);
    try {
      setReply("", true);
      const body = JSON.stringify({ messages });
      const responseVercel = await fetch("/api/openai", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: body,
      });
      console.log("Edge function returned.");
      if (!responseVercel.ok) {
        throw new Error(responseVercel.statusText);
      }

      // This data is a ReadableStream
      const data = responseVercel.body;
      if (!data) {
        return;
      }

      const reader = data.getReader();
      const decoder = new TextDecoder();
      let done = false;
      while (!done) {
        const { value, done: doneReading } = await reader.read();
        done = doneReading;
        const chunkValue = decoder.decode(value);
        setReply(chunkValue);
      }
    } catch (error: any) {
      alert(error);
    }
    setLoading(false);
  };

  return {
    fetchStream,
  };
};

export { useOpenAI };

Front

あとは先ほどのhookをfrontから呼び出したら完成です。
過去のプロンプトとAIからの回答と最新のプロンプトをOpenAIに渡します。
roleがuser, assistantと交互に入ります。
assistantはChatGPTの返答です。

//抜粋

const messages: Message[] = [
  { role: "user", content: "こんにちは" },
];

const setReply = (newVal: string, isNew?: boolean) => {
  setValue((v) => isNew ? newVal: v + newVal)
};

await fetchStream(messages, setAILoading, setReply);

これで完成です。

ぜひ試してみてください。
すぐOpenAIの無料分なくなるかもですが笑

一緒に開発してくれる方募集!

もし参加したい方いらっしゃいましたらお声がけしていただけるとうれしいです!

Discussion

melodyclue_routermelodyclue_router

streamで実践している記事があまりなかったので助かりました!

ところで、こちらをNestJSでやってみたのですが、うまくいきませんでした。
NextJSがstreamをよしなにやってくれているため、NestJSではもう少し改良する必要?があるようです。