Node.js TypeScript で OpenAI

Node.js TypeScript で OpenAI

2023-03-2623 min read

目次

  1. 概要
  2. 環境構築
  3. アクセストークンの取得
  4. 試してみる
  5. stream-として扱う
  6. express-を導入して-バインド-api-を作る
  7. おまけ-fetch-の利用
  8. まとめ
  9. 参考にしたサイト

概要

この記事では、Node.js と TypeScript を用いて OpenAI (ChatGPT)の API を利用してみました。

環境構築

まずは、必要なパッケージをインストールします。

npm init -y
npm install typescript @types/node ts-node

次に、OpenAI のパッケージをインストールします。

npm install openai

アクセストークンの取得

https://platform.openai.com/account/api-keys

からアクセストークンを取得します。

試してみる

以下のコードで、世界の大きな山を順番に 5 つ出力する例を試します。

import {
  Configuration,
  OpenAIApi,
  ChatCompletionRequestMessageRoleEnum,
} from "openai";

const apiKey = `<OPENAI_API_KEY>`;

const configuration = new Configuration({
  apiKey,
});
const openai = new OpenAIApi(configuration);

const models = {
  gpt3_5: "gpt-3.5-turbo-0301",
};

const main = async () => {
  const content = "世界のなかで大きい山を順番に5個出力してください";
  const res = await openai.createChatCompletion({
    model: models.gpt3_5,
    messages: [{ role: ChatCompletionRequestMessageRoleEnum.User, content }],
  });
  console.log(res.data.choices[0].message?.content);
};

main();

成功すると、以下のようなレスポンスが返ってきます。

1. エベレスト山(ネパール・中国)
2. キリマンジャロ山(タンザニア)
3. アコンカグア山(アルゼンチン)
4. デナリ山(アメリカ・アラスカ州)
5. モンブラン山(フランス・イタリア)

Stream として扱う

Stream を利用して操作してみます。

ストリームを利用して処理する方法を紹介します。 この方法を使うと、すべてのデータが返されるのを待たずに、1 文字ずつリアルタイムでレスポンスを受け取ることができます。 これにより、OpenAI のチャット画面のようなスムーズな描画が実現できます。

サンプルコード

const main = async () => {
  const content = "世界のなかで大きい山を順番に5個出力してください";
  const res = await openai.createChatCompletion(
    {
      model: models.gpt3_5,
      stream: true,
      messages: [{ role: ChatCompletionRequestMessageRoleEnum.User, content }],
    },
    { responseType: "stream" }
  );
  let result = "";
  for await (const chunk of res.data as any) {
    const lines = chunk
      .toString("utf8")
      .split("\n")
      .filter((line) => line.trim().startsWith("data: "));
    // console.log(chunk.toString("utf8")); // 1行ごとデバッグ

    for (const line of lines) {
      const message = line.replace(/^data: /, "");
      if (message === "[DONE]") {
        console.log(result);
        return result;
      }
      const json = JSON.parse(message);
      const token = json.choices[0].delta.content;
      if (token) {
        result += token;
      }
    }
  }
};

main();

パースについて

Stream を用いたレスポンスの 1 レコードは次のような形式です。

data: {"id":"xxxx","object":"chat.completion.chunk","created":1679809115,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"エ"},"index":0,"finish_reason":null}]}

ここから 先頭の data: をトリミングした上で JSON デコードし、choices[n].delta.content を抽出します。 一番最後の行のみ[DONE]となっているので、これを検知したらレスポンスは終了となります。

express を導入して バインド API を作る

Node.js の Web アプリケーションフレームワークである Express を導入し、API を作成する方法を紹介します。

まず、必要なパッケージをインストールします。

npm install express
npm install -D @types/express

サンプルコード

サンプルコードでは、chatStream 関数を定義して、ストリームで受け取ったレスポンスを処理しています。

import {
  Configuration,
  OpenAIApi,
  ChatCompletionRequestMessageRoleEnum,
} from "openai";
import * as express from "express";
import { Readable } from "stream";
const app = express();

const apiKey = `<OPENAI_API_KEY>`;

const configuration = new Configuration({
  apiKey,
});
const openai = new OpenAIApi(configuration);

const models = {
  gpt3_5: "gpt-3.5-turbo-0301",
};

const chatStream = async (
  content: string,
  stream: Readable,
  res: express.Response
) => {
  const openaiRes = await openai.createChatCompletion(
    {
      model: models.gpt3_5,
      stream: true,
      messages: [{ role: ChatCompletionRequestMessageRoleEnum.User, content }],
    },
    { responseType: "stream" }
  );
  for await (const chunk of openaiRes.data as any) {
    const lines = chunk
      .toString("utf8")
      .split("\n")
      .filter((line) => line.trim().startsWith("data: "));
    try {
      for (const line of lines) {
        const message = line.replace(/^data: /, "");
        if (message.trim() == "[DONE]") {
          stream.push(null);
          res.end();
          return;
        }
        const json = JSON.parse(message);
        const token = json.choices[0].delta.content;
        if (token) {
          stream.push(Buffer.from(token));
          await sleep(100);
        }
      }
    } catch (err) {
      stream.push(null);
      res.end();
    }
  }
};

app.get("/", async (req, res) => {
  res.setHeader("Content-Type", "text/plain; charset=utf-8");
  res.setHeader("Transfer-Encoding", "chunked");
  const stream = new Readable({ read() {}, highWaterMark: 1 });
  stream.pipe(res);
  const q = String(req.query.q);
  chatStream(q, stream, res);
});

app.listen(8080, () => {
  console.log(`Example app listening on port 8080`)
})

リクエスト例

http://localhost:8080/?q=世界のなかで大きい山を順番に100個出力してください

こんな感じでリクエストしたらレスポンスが逐次描画されます。

おまけ: fetch の利用

fetch API を利用しても同様の実装は可能です。

const main = async (content) => {
  try {
    const res = await fetch("https://api.openai.com/v1/chat/completions", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        messages: [
          {
            role: "user",
            content: content,
          },
        ],
        model: "gpt-3.5-turbo",
        max_tokens: 2048,
        stream: true,
      }),
      signal: new AbortController().signal,
    });
    const decoder = new TextDecoder("utf8");
    if (!res.body) {
      throw new Error("error");
    }
    const reader = res.body?.getReader();
    let result = "";
    while (1) {
      const { value, done } = await reader.read();
      const lines = decoder
        .decode(value)
        .toString()
        .split("\n")
        .filter((line) => line.trim().startsWith("data: "));
      // console.log(row)
      for (const line of lines) {
        const message = line.replace(/^data: /, "");
        if (message === "[DONE]") {
          console.log(result);
          return result;
        }
        const json = JSON.parse(message);
        const token = json.choices[0].delta.content;
        if (token) {
          result += token;
        }
      }
    }
  } catch (error) {
    return error;
  }
};

main("世界のなかで大きい山を順番に5個出力してください");

まとめ

この記事では、Node.js と TypeScript を用いて OpenAI の API を利用する方法を紹介しました。 環境構築から、ストリームでの処理、さらには Fetch API の利用方法まで、説明しました。

参考にしたサイト

Tags
javascript(110)
node.js(54)
linux(54)
amazon%20aws(47)
typescript(45)
%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0(36)
%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86(30)
html5(29)
php(24)
centos(24)
python(22)
%E7%AB%B6%E6%8A%80%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0(21)
mac(21)
mysql(20)
canvas(19)
opencv(17)
%E9%9B%91%E8%AB%87(16)
docker(16)
wordpress(15)
atcoder(14)
apache(12)
%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92(12)
%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9(12)
amazon%20s3(12)
red%20hat(12)
prisma(12)
ubuntu(11)
github(10)
git(10)
vue.js(10)
%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86100%E6%9C%AC%E3%83%8E%E3%83%83%E3%82%AF(10)
mariadb(10)
react(9)
aws%20cdk(9)
css3(8)
%E5%8F%AF%E8%A6%96%E5%8C%96(8)
%E5%B0%8F%E3%83%8D%E3%82%BF(8)
nestjs(8)
amazon%20lightsail(7)
next.js(7)
%E3%83%96%E3%83%AD%E3%82%B0(6)
cms(6)
oracle(6)
perl(6)
gitlab(6)
iam(5)
amazon%20ec2(5)
%E8%B3%87%E6%A0%BC%E8%A9%A6%E9%A8%93(5)
aws%20amplify(5)
curl(4)
Author
githubzennqiita
ただの備忘録です。

※外部送信に関する公表事項