見出し画像

openai-nodeを使ってchat/completionsのstreamをフロントエンドで試す

ChatGPTを作っているOpenAI社は、サードパーティ向けにOpenAI APIを提供しています。クライアントライブラリも提供していて、一般的なREST APIとして各種機能が簡単に使えます。

https://platform.openai.com/docs/libraries

openai-nodeとstream

Node.js用に提供しているopenai-nodeは、フロントエンドでも利用できます。まずは実験的に気軽に試してみるのに最適です。

OpenAI APIは通常のRequest/Responseの他にstream形式の通信(Sever Sent Event)をサポートしています。プロンプトの内容によっては通常のRequest/Responseではレイテンシが大きくなってしまう場合があり(5000msとか)、実用面で課題があったのですが、stream形式で通信を行えば、レスポンスのデータが逐次届くので、素早く画面への反映を開始できます(大体200,300ms)。

Request/Responseの場合
streamの場合

ところが、openai-nodeでは、気軽にstreamを扱えません。Github上でかなり長いあいだ議論が続いています。いくつかのworkaroundはあるものの、フロントエンドでの利用はあまり語られていません。ライブラリが返すレスポンスオブジェクトにstreamを受け取るための関数がないので、APIにstreamを要求してもアプリケーション側ではすべてのレスポンスが返却されないと値が取り出せないようになっています。

とりあえずstreamをフロントエンドで試す

おそらく今後正式なサポートがされていくと思うのですが、取り急ぎフロントエンドで気軽に試したいよな〜、ということで、フロントエンドで試すワークアラウンドを示します。

openai-nodeは、内部でaxiosを利用しており、APIの引数にAxiosRequestConfigを渡せます。AxiosRequestConfigにはonDownloadProgressというプロパティがあり、通信の進捗状況をProgressEventとして渡してくれます。ProgressEventには受診したデータが入っているので、この中からAPIのレスポンスデータを抽出します。

import { Configuration, OpenAIApi } from 'openai';

const configuration = new Configuration({
  apiKey: $apiKey
});

const openai = new OpenAIApi(configuration);

export const assistantStream = async (
  text: string,
  onData: (data: string) => void
): Promise<void> => {
  await openai.createChatCompletion(
    {
      model: 'gpt-3.5-turbo',
      messages: [
        { role: 'system', content: '3行程度に要約してください。' },
        { role: 'user', content: text }
      ],
      temperature: 0.6, max_tokens: 300, stream: true
    },
    {
      responseType: 'stream',
      onDownloadProgress: (progressEvent) => {
        const regex = /"delta":{"content":"(.*)"},/g;
        const array = [...progressEvent.target.responseText.matchAll(regex)];
        const text = array.map((a) => a[1]).join('');
        onData(text);
      }
    }
  );
};

streamデータの構造は概ね次の通りで、ProgressEventにはこのテキストデータがたまり続けます。

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

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

data...

ProgressEventにたまるデータは差分ではなく積み上げなので、毎回抽出結合処理をすれば、そのタイミングでの全体のデータが取り出せます。次のように正規表現で抽出してmapしてjoinすればレスポンスのテキストを得られます。この辺りはフォーマットが変わると壊れるので、そのまま実用するわけにはいかなそうです。

const regex = /"delta":{"content":"(.*)"},/g;
const array = [...progressEvent.target.responseText.matchAll(regex)];
const text = array.map((a) => a[1]).join('');

逐次的な表示のUXを気軽に試す

フロントエンドでのopenai-nodeの利用は、プロダクションでは現実的ではないですが、色々なアイデアをさっと試したり、逐次的な表示を体験できたりします(思ったより良い体験だな〜と感じました)。ちょっと試したいな〜といった時に役に立てれば幸いです。

この記事が気に入ったらサポートをしてみませんか?