💪

OpenAIのAssistantsAPIをNodeで触ってみる

2023/11/09に公開

はじめに

11/7のDevDayで発表されたAPI達のサンプルはPythonの事例は多いけどNodeがあまりなかったから残しておきます。
アシスタントにはいろいろToolsがあるけど今回はFunction Callingを試すことにしました。
ついでにSveltKitも触ったことがなかったので触ってみる。

構成

今回発表された「JSONモード」と「Function Calling」の違い

Function CallingとJSONモードの違いは出力が特定のスキーマに合致することを保証されているかどうからしいです。
JSONモードは出力が解析可能なJSONであることは保証されるが指定したスキーマに合わせて出力されるわけではないので実際の実装を想定して今回はFunction Callingを使ってみました。
https://community.openai.com/t/json-mode-vs-function-calling/476994

実践

OpenAIのPlaygroundを使用して事前にアシスタントを作成してそこに接続するようにします。
もちろんコード内でアシスタントを作成することもできますが今回は割愛します。

APIルートを作成する

routes/api/chat/+server.ts
import OpenAI from "openai";
import { OPENAI_API_KEY, ASSISTANT_ID } from "$env/static/private";
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";

const openai = new OpenAI({
  apiKey: OPENAI_API_KEY
});

// 既存のアシスタントへ接続する
const assistant = await openai.beta.assistants.retrieve(ASSISTANT_ID);
// 新しいスレッドを作成する
const thread = await openai.beta.threads.create();

export const POST = (async ({ request }) => {
  // ユーザーの入力を取得する
  const body = await request.json();
  const userQuestion = body.question;

  console.log(`User: ${userQuestion}`);

  // ユーザーの質問をスレッドに渡す
  await openai.beta.threads.messages.create(thread.id, {
    role: "user",
    content: userQuestion
  });

  // アシスタントの応答を待ち、それを取得するためにrunを使用する
  const run = await openai.beta.threads.runs.create(thread.id, {
    assistant_id: assistant.id
  });

  let runStatus = await openai.beta.threads.runs.retrieve(thread.id, run.id);

  // 実行ステータスをポーリングして確認する
  while (runStatus.status !== "requires_action") {
    await new Promise((resolve) => setTimeout(resolve, 2000));
    runStatus = await openai.beta.threads.runs.retrieve(thread.id, run.id);
  }

  // アシスタントのfunctionsを受け取る
  const action = runStatus.required_action?.submit_tool_outputs.tool_calls[0].function;
  console.log(action?.name);

  runStatus = await openai.beta.threads.runs.submitToolOutputs(thread.id, run.id, {
    tool_outputs: [
      {
        tool_call_id: runStatus.required_action?.submit_tool_outputs.tool_calls[0].id,
        output: action?.arguments
      }
    ]
  });

  // 実行ステータスをポーリングして確認する
  // TODO: ストリーミング対応されたら、ポーリングをやめる
  while (runStatus.status !== "completed") {
    await new Promise((resolve) => setTimeout(resolve, 2000));
    runStatus = await openai.beta.threads.runs.retrieve(thread.id, run.id);
    console.log(runStatus.status);
  }

  let assistantMessage = "";

  // メッセージ配列から最後のアシスタントメッセージを取得する
  const messages = await openai.beta.threads.messages.list(thread.id);

  // 現在のランの最後のメッセージを見つける
  const lastMessageForRun = messages.data
    .filter((message) => message.run_id === run.id && message.role === "assistant")
    .pop();

  // 型をチェック
  if (
    lastMessageForRun &&
    lastMessageForRun.content.length > 0 &&
    "text" in lastMessageForRun.content[0]
  ) {
    assistantMessage = lastMessageForRun.content[0].text.value;
  }
  console.log(json(assistantMessage));

  return json(assistantMessage);
}) satisfies RequestHandler;

ページ側を記述

routes/+page.svelte
<script lang="ts">
  let question = "";
  let answer = "";
  let isLoading = false;

  async function submitQuestion() {
    isLoading = true;
    answer = "";
    const response = await fetch("/api/chat", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({ question })
    });
    if (response.ok) {
      const data = await response.json();
      answer = data;
    } else {
      answer = "エラーが発生しました。";
    }
    isLoading = false;
  }
</script>

<main>
  <h1>Chat with AI</h1>
  <input
    type="text"
    bind:value={question}
    placeholder="質問を入力してください"
    on:keypress={(e) => e.key === "Enter" && submitQuestion()}
  />
  <button on:click={submitQuestion} disabled={!question || isLoading}> 質問する </button>
  {#if isLoading}
    <p>Loading...</p>
  {/if}
  {#if answer}
    <p>回答: {answer}</p>
  {/if}
</main>

<style>
  main {
    text-align: center;
    padding: 1em;
    max-width: 240px;
    margin: 0 auto;
  }
  input {
    width: 100%;
    margin-bottom: 0.5em;
  }
  button {
    width: 100%;
    padding: 0.5em;
  }
  p {
    margin-top: 1em;
  }
</style>

注意点

このサンプルコードではPOSTされる度に新しくスレッドを作成しているため、会話を続けることができないです。
もし会話を実装したければユーザーを作成してそこで新たにスレッドを作成後、以降はそのスレッドにアクセスするようにすればいいと思います。

  const myThread = await openai.beta.threads.retrieve(
    "thread_abc123"
  );

https://platform.openai.com/docs/api-reference/threads/getThread

感想

まだベータ版のためポーリングする必要がある

OpenAI曰く近日中にはストリーミングをサポートされるようですが現時点(11月9日)ではサポートされていないのでこの辺りが改善されたらAssistants APIが今後の開発の主流になりそうな予感がしてます。
今後のアップデートとしてDALL-EやVisonAPIがサポートされるようなので楽しみにしてます!
https://platform.openai.com/docs/assistants/how-it-works/limitations

MOXT Tech Blog

Discussion