1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Node.js】 OpenAI Agents SDK の TypeScript版でハンドオフを使ってみる(途中の状態を出力する処理も加える)

Posted at

はじめに

6月はじめに以下の内容を試していた、TypeScript版の OpenAI Agents SDK に関するお試しです。

●【Node.js】 OpenAI Agents SDK の TypeScript版で MCPサーバーのツールを使う - Qiita
 https://qiita.com/youtoy/items/c2215b5bf2c903fc62bc

●【Node.js】 OpenAI Agents SDK の TypeScript版で Hello World とサンプルを軽く試す - Qiita
 https://qiita.com/youtoy/items/0972e1f4ba5213f76cb1

今回の内容

今回、以下の公式ドキュメントのページで示されている「ハンドオフ」を試します。

●ハンドオフ | OpenAI Agents SDK
 https://openai.github.io/openai-agents-js/ja/guides/handoffs/

試してみる

実際に試していきます。

下準備

まずは下準備です。以下の npmパッケージをインストールします。

●@openai/agents - npm
 https://www.npmjs.com/package/@openai/agents

上記ページの「Installation」の部分には、以下のように記載されています。

image.png

このコマンドについて、以下のコマンドでも zod がインストールされるようだったので、以下で試しました。

npm install @openai/agents

また処理を実行するための APIキーの設定も行います。環境変数で、OPENAI_API_KEY という変数名で、OpenAI の APIキーをセットします。

動作確認

とりあえず、TypeScript版 OpenAI Agents SDK の動作確認を行います。用いたコードは、公式が「Hello world example」という内容で掲載している、以下の内容です。

import { Agent, run } from "@openai/agents";

const agent = new Agent({
  name: "Assistant",
  instructions: "You are a helpful assistant",
});

const result = await run(
  agent,
  "Write a haiku about recursion in programming."
);
console.log(result.finalOutput);

とりあえず、以下の返答を得ることができました。

image.png

ハンドオフのサンプルを試してみる

次にハンドオフのサンプルを試してみます。とりあえず、サンプルをそのままの内容で動かします。

以下に、用いたコードと実行結果を示します。

import { z } from "zod";
import { Agent, run, tool } from "@openai/agents";

const getWeatherTool = tool({
  name: "get_weather",
  description: "Get the weather for a given city",
  parameters: z.object({ city: z.string() }),
  execute: async (input) => {
    return `The weather in ${input.city} is sunny`;
  },
});

const dataAgent = new Agent({
  name: "Data agent",
  instructions: "You are a data agent",
  handoffDescription: "You know everything about the weather",
  tools: [getWeatherTool],
});

// Use Agent.create method to ensure the finalOutput type considers handoffs
const agent = Agent.create({
  name: "Basic test agent",
  instructions: "You are a basic agent",
  handoffs: [dataAgent],
});

async function main() {
  const result = await run(agent, "What is the weather in San Francisco?");
  console.log(result.finalOutput);
}

main().catch(console.error);

image.png

The weather in San Francisco is currently sunny. If you need additional details such as temperature, humidity, or forecast, just let me know! という回答を得られました。

コードの構成と処理の流れ

コードを見ると、処理の流れとしては、「getWeatherTool」を使える「dataAgent」が、「Basic test agent」からのハンドオフで呼ばれて処理を行うようなもののようです。

処理を実行して得られた内容を見た感じでは、「getWeatherTool」が呼び出されたように見えます。しかし、出力から処理の流れが見えないので、それを見えるようにコードを書きかえて実行してみます。

ハンドオフのサンプルで実行中の情報を出力してみる

先ほどのハンドオフのサンプルで、エージェントの切り替えやツール利用など、処理途中の情報も出力するようにしてみます。少しエージェントの名前などの書きかえも行います。

書きかえた内容は、具体的には以下の内容です。

import { z } from "zod";
import { Agent, run, tool } from "@openai/agents";

const getWeatherTool = tool({
  name: "get_weather",
  description: "Get the weather for a given city",
  parameters: z.object({ city: z.string() }),
  execute: async (input) => {
    return `The weather in ${input.city} is sunny`;
  },
});

const dataAgent = new Agent({
  name: "Data agent",
  instructions: "You are a data agent",
  handoffDescription: "You know everything about the weather",
  tools: [getWeatherTool],
});

const rootAgent = Agent.create({
  name: "Basic test agent",
  instructions: "You are a basic agent",
  handoffs: [dataAgent],
});

async function main() {
  const result = await run(rootAgent, "What is the weather in San Francisco?");

  console.log("=== 処理の流れ ===");
  for (const item of result.newItems) {
    switch (item.type) {
      case "tool_call_item":
        console.log(`[Tool利用]   ${item.agent.name}${item.rawItem.name}`);
        break;

      case "tool_call_output_item": {
        const rawOut = item.rawItem.output;
        let outputText;
        if (typeof rawOut === "string") {
          outputText = rawOut;
        } else if (rawOut && rawOut.text) {
          outputText = rawOut.text;
        } else {
          outputText = JSON.stringify(rawOut);
        }
        console.log(`[Toolの実行結果]   ${item.rawItem.name}${outputText}`);
        break;
      }

      case "handoff_output_item":
        console.log(
          `[Handoff] ${item.sourceAgent.name}${item.targetAgent.name}`
        );
        break;

      case "message":
        console.log(`[Msg] ${item.agent.name}: ${item.rawItem.content.text}`);
        break;

      default:
        break;
    }
  }

  console.log("\n=== 最終出力 ===");
  console.log(result.finalOutput);
}

main().catch(console.error);

rawOut の部分はツール実行結果が格納されたプロパティで、以下を想定しています。

  • 文字列型
  • オブジェクト型で、OpenAI のチャットレスポンスの形のような { text: "...", ...: ... } というもの
  • ツールが返す上記以外のオブジェクト

それで、文字列はそのまま出力し、 .text プロパティがあればそれを出力するようにしてみています。また、それらのどちらにも当てはまらない場合は JSON.stringify でオブジェクト全体を文字列化しました。

処理の実行結果

実行結果は以下のとおりです。

image.png

エージェントは Basic test agent から Data agent にかわり、その後、Data agent のツールの get_weather が使われていることが確認できました。そして、ツールの実行結果として「The weather in San Francisco is sunny」という内容が得られていることも確認できました。

ハンドオフのサンプルにエージェントを追加してみる

最後に、ハンドオフのサンプルにエージェントを追加して実行してみます。そして、それらのエージェント全て(最初に処理を受け付けるエージェントと、天気・数学・翻訳・ニュースの 4つのエージェント)が実行されるようにしてみます。

具体的なコードは以下のとおりで、プロンプトの部分は日本語にしてみました。なお、ツールで行う処理は外部から情報を得るようなものでなく、ハードコーディングした固定の内容を返すものにしています(処理のテスト用、という感じです)。

import { z } from "zod";
import { Agent, run, tool } from "@openai/agents";

const getWeatherTool = tool({
  name: "get_weather",
  description: "Get the weather for a given city",
  parameters: z.object({ city: z.string() }),
  execute: async ({ city }) => `The weather in ${city} is sunny`,
});

const addTool = tool({
  name: "add_numbers",
  description: "Add two numbers",
  parameters: z.object({ a: z.number(), b: z.number() }),
  execute: async ({ a, b }) => `${a} + ${b} = ${a + b}`,
});

const translateTextTool = tool({
  name: "translate_text",
  description: "Translate text to a specified language",
  parameters: z.object({ text: z.string(), to: z.string() }),
  execute: async ({ text, to }) => `Translated "${text}" to ${to}`,
});

const newsTool = tool({
  name: "get_news",
  description: "Get today's tech headline",
  parameters: z.object({}),
  execute: async () => "OpenAI releases new SDK!",
});

const weatherAgent = new Agent({
  name: "Weather agent",
  instructions: "Answer weather questions using get_weather",
  handoffDescription: "Knows about weather",
  tools: [getWeatherTool],
});

const mathAgent = new Agent({
  name: "Math agent",
  instructions: "Perform arithmetic using add_numbers",
  handoffDescription: "Knows arithmetic",
  tools: [addTool],
});

const translationAgent = new Agent({
  name: "Translation agent",
  instructions:
    "Translate user text to the specified language using translate_text",
  handoffDescription: "Knows translations",
  tools: [translateTextTool],
});

const newsAgent = new Agent({
  name: "News agent",
  instructions: "Provide tech headlines using get_news",
  handoffDescription: "Knows tech news",
  tools: [newsTool],
});

const router = Agent.create({
  name: "Router agent",
  instructions:
    "Decide whether to answer yourself or hand off to the specialist whose description best matches the user question.",
  handoffs: [weatherAgent, mathAgent, translationAgent, newsAgent],
});

async function main() {
  const questions = [
    "東京の天気は?",
    "12 + 345 は?",
    "'Hello' をスペイン語に翻訳してください。",
    "今日の技術系ニュースは?",
  ];

  for (const q of questions) {
    console.log(`\n質問: ${q}`);

    const result = await run(router, q);

    for (const item of result.newItems) {
      switch (item.type) {
        case "tool_call_item":
          console.log(`${item.agent.name} calls ${item.rawItem.name}`);
          break;
        case "tool_call_output_item": {
          const out = item.rawItem.output;
          const text =
            typeof out === "string" ? out : out?.text ?? JSON.stringify(out);
          console.log(`${item.rawItem.name}: ${text}`);
          break;
        }
        case "handoff_output_item":
          console.log(`${item.sourceAgent.name}${item.targetAgent.name}`);
          break;
        case "message":
          console.log(`${item.agent.name}: ${item.rawItem.content.text}`);
          break;
      }
    }

    console.log(result.finalOutput);
  }
}

main().catch(console.error);

実行結果

上記の実行結果は以下のとおりです。

image.png

質問の内容によって適切なエージェントが呼び出されて、そしてそれぞれのエージェントから回答を得られていることが分かりました。

また再度、上記を実行してみて、その時の様子を動画にもしてみました。

おわりに

今回、TypeScript版の OpenAI Agents SDK に関し、公式ドキュメントのページで示されている「ハンドオフ」を試しました。さらに、適当なエージェントを追加したものを作り、4つのエージェント(天気・数学・翻訳・ニュースの 4つ)がユーザーのプロンプトによって使い分けられるかを試してみました。

とりあえずシンプルな内容は試せたという感じなので、より複雑なものなども試せればと思います。

その他

記事執筆時点でサポートされている機能について

OpenAI Agents SDK の TypeScript版について、記事執筆時点でサポートされている機能と、サポートがまだの機能は以下のようになっているようです。

●openai/openai-agents-js: A lightweight, powerful framework for multi-agent workflows and voice agents
 https://github.com/openai/openai-agents-js

image.png

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?