はじめに
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」の部分には、以下のように記載されています。
このコマンドについて、以下のコマンドでも 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);
とりあえず、以下の返答を得ることができました。
ハンドオフのサンプルを試してみる
次にハンドオフのサンプルを試してみます。とりあえず、サンプルをそのままの内容で動かします。
以下に、用いたコードと実行結果を示します。
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);
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 でオブジェクト全体を文字列化しました。
処理の実行結果
実行結果は以下のとおりです。
エージェントは 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);
実行結果
上記の実行結果は以下のとおりです。
質問の内容によって適切なエージェントが呼び出されて、そしてそれぞれのエージェントから回答を得られていることが分かりました。
また再度、上記を実行してみて、その時の様子を動画にもしてみました。
おわりに
今回、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