注目イベント!
アドベントカレンダー2024開催します(12/1~12/25)!
一年を締めくくる特別なイベント、アドベントカレンダーを今年も開催します!
初心者からベテランまで楽しめる内容で、毎日新しい技術トピックをお届けします。
詳細はこちらから!
event banner

OpenAIのAssistants API(ベータ版)を試す

| 9 min read
Author: noboru-kudo noboru-kudoの画像

先日のOpenAIの開発者イベント(DevDay)では、新モデル(GPT-4 turbo)やカスタムGPT(GPTs)等、数多くの注目機能が発表されましたね。

この中で、新たなAPIとして発表されたAssistants APIに今回注目してみます。

Assistants APIは会話履歴をスレッドとして管理してくれます。今までのChat Completion APIでは、文脈を維持するために履歴管理を自前でやる必要がありましたがOpenAIに委ねることができるようになりました。

また、Assistants APIはCode InterpreterやFunction Calling等の各種ツールに加えて、ファインチューニングしたカスタムモデルを使うこともできます。
これらを調停してくれるのがアシスタントです。アシスタントはリクエストの内容からGPTを使うのか各種ツールを使うのかを判断するエージェント機能としての役割を果たします。

このように、Assistants APIを使えばアプリケーションに高度なGPT機能を組み込むことが簡単になり、その使い方も多様化してくることが想像できますね。

早速これを試してみたいと思います。
ここではNode.jsで実行します。Assistants APIはOpenAIモジュールのv4.16.0以降のバージョンに含まれています。

なお、本文中の引用は全てAssistants APIの公式ドキュメントが出典元になります。

アシスタント作成

#

まずは、Assistants APIのアシスタントを作成します。

const assistant = await openai.beta.assistants.create({
  name: 'My Assistant',
  instructions: 'You act as Frieza from Dragon Ball. Speak in Japanese',
  tools: [{ type: 'code_interpreter' }, {
    type: 'function',
    function: {
      name: 'calcStrength',
      description: 'Calculate Battle Strength',
      parameters: {
        type: 'object',
        properties: {
          name: {
            type: 'string',
            description: 'The user name'
          }
        }
      }
    }
  }],
  model: 'gpt-4',
  file_ids: []
});

instructionsはChatGPTのカスタムインストラクションと同じで、アシスタント自体のセットアップをします。
ここでは、某アニメキャラクターとして振る舞うようにするのと、日本語を返すようにしました。

toolsの部分ではアシスタントが使えるツールを指定します。ここではCode InterpreterFunction Callingを設定しました。
前述の通り、ここで指定したツールはリクエスト内容に応じてアシスタントが利用有無を判断します。

利用できるツールとしては、この他にも任意のファイルからインデックス検索するRetrievalも使えます。
Assistants APIのドキュメントによると、将来的には自作ツールを含めた拡張も予定しているようです。

In the future, we plan to release more OpenAI-built tools, and allow you to provide your own tools on our platform

現時点で利用可能なツールの詳細は、以下公式ドキュメントを参照してください。

最後にmodelです。GPT-3.5 または GPT-4の他にファインチューニングしたカスタムモデルも利用可能とのことです。

you can specify any GPT-3.5 or GPT-4 models, including fine-tuned models. The Retrieval tool requires gpt-3.5-turbo-1106 and gpt-4-1106-preview models.

Information

ここで作成したアシスタントはOpenAIのUIから参照できます(新規作成もできます)。

一度作成したアシスタントは、APIからはopenai.beta.assistants.retrieve(assistantId)で取得して再利用できます。
長く使うアシスタントは、都度作成するのではなく事前に登録しておく方が良いのかもしれませんね(現時点で有効期限は確認できませんでした)。

スレッド作成

#

次にスレッドを作成します。スレッドはユーザーとアシスタントの会話履歴を管理します。

const thread = await openai.beta.threads.create({});

ここでパラメータに何も指定しませんでしたが、初期メッセージもここで設定できます。

公式ドキュメントによると、スレッドにサイズリミットはなく、利用モデルの上限に応じたメッセージの削除も自動でやってくれるそうです。

Threads don’t have a size limit. You can pass as many Messages as you want to a Thread. The API will ensure that requests to the model fit within the maximum context window, using relevant optimization techniques such as truncation.

会話の文脈を維持するためにメッセージをどこかに保存しておいたり、モデルのトークン上限を超えないように履歴サイズ調整する手間から解放されます。

メッセージ作成

#

作成したスレッドにメッセージを追加していきます。

const message = await openai.beta.threads.messages.create(
  thread.id,
  {
    role: 'user',
    content: '豆香の戦闘力を教えてください'
  }
);

先ほど作成したスレッドのIDを指定してメッセージを追加しています。スレッドには複数のメッセージが追加できます。
なお、ここで指定可能なroleはuserのみです。

スレッド実行

#

メッセージを追加したので、スレッドを実行します。

const run = await openai.beta.threads.runs.create(
  thread.id,
  {
    assistant_id: assistant.id
  }
);

スレッドとアシスタントのIDをそれぞれ指定するだけです。
ここでは特に指定していませんが、第2引数でアシスタント生成時に指定したツールやモデル、カスタムインストラクション等を上書き可能です。

レスポンス取得

#

スレッド実行は非同期です。実行状態は以下で取得します。

const currentRun = await openai.beta.threads.runs.retrieve(
  thread.id,
  run.id
);

スレッドとスレッド実行(run)のIDを設定します。
アシスタントからのレスポンスを参照するには、このcurrentRun.statuscompletedになるまで待つ必要があります。

注意点として、任意のユーザー関数を実行するFunction Callingの場合は、statuscompletedではなくrequires_actionになります。
これはユーザーサイド(クライアント側)でのアクションを要求するという意味です。
この場合は、以下のように実行結果を再度アシスタントに連携する必要があります。

if (currentRun.status === 'requires_action') {
  // 実行する関数の名前や引数を取得
  console.log('function calling -> ', currentRun.required_action?.submit_tool_outputs.tool_calls);
  // --- ここで関数実行 ---
  // 自前の関数を呼んだ体にして結果を取得
  await openai.beta.threads.runs.submitToolOutputs(
    thread.id,
    run.id,
    {
      tool_outputs: [{
        tool_call_id: currentRun.required_action?.submit_tool_outputs.tool_calls[0].id, // 複数の場合もあり
        output: '戦闘力は53万です...'
      }]
    }
  );
}

スレッド実行(run)のcurrentRun.required_action.submit_tool_outputs.tool_callsに実行するユーザー関数名やその引数が含まれます。上記の場合は以下のような情報が含まれます。

[
  {
    id: 'call_zVi1576XEUYIw0MyDjxU8ZW4',
    type: 'function',
    function: { name: 'calcStrength', arguments: '{\n  "name": "豆香"\n}' }
  }
]

ここで取得したID(tool_call_id)と関数実行結果を再連携すると、再度アシスタントは実行状態(in_progress)に戻ります[1]

Information

Function callingについては、以下記事で紹介していますので詳細は省略しています。

こちらの記事はAssistants APIについてではありませんが、設定内容は同じです。

最終的にステータスがcompletedになれば実行完了です。
アシスタントのレスポンスを取得します。

const messages = await openai.beta.threads.messages.list(
  thread.id
);
for (const message of messages.data) {
  if (message.role === 'user') break; // ユーザーメッセージに到達したら終了
  // 以下はアシスタント(role===assistant)メッセージ
  const [content] = message.content;
  switch (content.type) {
    case 'text':
      console.log(content.text.value);
      break;
    case 'image_file':
      console.log('image_file', content.image_file.file_id);
  }
}

スレッドIDを指定して、メッセージを取得して直近のアシスタントメッセージを表示しています。

なお、レスポンスタイプとしてイメージファイル(image_file)もサポートされています(この場合は現時点で最新のモデルgpt-4-1106-previewを指定する必要がありました)。
上記はファイルIDを出力しているだけですが、この後でファイル取得APIを実行すれば、イメージファイルも取得できます[2]

CLIベースのChatGPTを作成する

#

即席ですが、今までの内容をまとめてCLIベースのChatGPTを書いてみました。

import OpenAI from 'openai';
import { input } from '@inquirer/prompts';

const openai = new OpenAI();

// アシスタント作成
const assistant = await openai.beta.assistants.create({
  name: 'My Assistant',
  instructions: 'You act as Frieza from Dragon Ball. Speak in Japanese',
  tools: [{ type: 'code_interpreter' }, {
    type: 'function',
    function: {
      name: 'calcStrength',
      description: 'Calculate Battle Strength',
      parameters: {
        type: 'object', properties: {
          name: {
            type: 'string',
            description: 'The user name'
          }
        }
      }
    }
  }],
  model: 'gpt-4',
  file_ids: []
});

// スレッド作成
const thread = await openai.beta.threads.create({});

while (true) { // 会話ループ
  const req = await input({ message: '>' });
  if (req === 'q') break; // `q`で終了
  // スレッドにメッセージ追加
  const message = await openai.beta.threads.messages.create(
    thread.id,
    {
      role: 'user',
      content: req
    }
  );

  // スレッド実行
  const run = await openai.beta.threads.runs.create(
    thread.id,
    {
      assistant_id: assistant.id
    }
  );

  // 完了するまでポーリング
  while (true) {
    await new Promise(resolve => setTimeout(resolve, 1000));
    const currentRun = await openai.beta.threads.runs.retrieve(
      thread.id,
      run.id
    );
    if (currentRun.status === 'completed') {
      break;
    } else if (currentRun.status === 'requires_action') {
      console.log('function calling -> ', currentRun.required_action?.submit_tool_outputs.tool_calls);
      // 自前の関数を呼んだ体にする
      await openai.beta.threads.runs.submitToolOutputs(
        thread.id,
        run.id,
        {
          tool_outputs: [{
            tool_call_id: currentRun.required_action?.submit_tool_outputs.tool_calls[0].id, // 複数の場合もあり
            output: '戦闘力は53万です...'
          }]
        }
      );
    } else if (currentRun.status === 'failed' || currentRun.status === 'cancelled' || currentRun.status === 'expired') {
      throw new Error(currentRun.status);
    }
  }

  // レスポンス取得
  const messages = await openai.beta.threads.messages.list(thread.id);
  for (const message of messages.data) {
    if (message.role === 'user') break;
    const [content] = message.content;
    switch (content.type) {
      case 'text':
        console.log(content.text.value);
        break;
      case 'image_file':
        console.log('image_file', content.image_file.file_id);
    }
  }
}

// クリーンアップ処理
await Promise.all([openai.beta.threads.del(thread.id), openai.beta.assistants.del(assistant.id)]);

これを実行すると、以下ような感じになりました。

大した量のコードは書いていませんが、なかなかいい感じのフリーザ様ですね。

まとめ

#

今回はOpenAIに導入されたベータ版のAssistants APIを試してみました。
少しのコードを書くだけで、結構何でもできる感じがします。
うまくアプリケーションに組み込めれば、革新的なものができそうで夢が膨らんできますね。

今度は今回できなかったファイル周りも再チャレンジしてみたいと思います。


  1. ここで紹介したもの以外にもいくつかステータスが存在します。ステータスの詳細は公式ドキュメントを参照してください。 ↩︎

  2. テキストベースのレスポンスでも注釈(annotations)にファイルIDが含まれていることもあります。詳細は公式ドキュメントを参照してください。 ↩︎

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。