ElixirでOpenAIのAPIによる新機能Function callingを試す
OpenAIのAPIに、新機能としてFunction callingが追加されました。これを用いると、GPTが必要に応じて指定された関数を呼び出すようレスポンスを返してきます。その際、パラメータをいい感じに渡してくれるので、今までJSON形式で返してくれなどとお願いしていたのが、プログラマブルかつ安定して実行可能になります。
Pythonでの例はFunction callingのドキュメントにある通りです。これに類することをElixirでやってみます。実行の流れがわかりやすくなるようあえてベタに書いているので、その点はご注意ください。
Livebookに依存モジュールをインストールする
ElixirからOpenAIのAPIを利用するには、mgallo/openai.exを用います。Livebookで用いるには、こんな感じでAPIキーを設定してやるとよいでしょう(関数呼び出し時に設定することもできます。詳しくはドキュメントを参照してください)。
Mix.install(
[
{:openai, "~> 0.5.2"}
],
config: [
openai: [
api_key: System.get_env("OPENAI_API_KEY")
]
]
)
関数の定義
OpenAIのAPIにfunctionに渡す関数を定義します。引数として指定された地域名に対応する天気予報を取得して返すという内容を定義しています。ここでは、関数名や引数の仕様のみを記述し、処理は実際の関数として別途定義します。
functions = [
%{
name: "get_weather",
description: "指定された地域の天気予報を取得します。",
parameters: %{
type: "object",
properties: %{
region: %{
type: "string",
description: "The name of the region"
}
}
},
required: ["region"]
}
]
[
%{
description: "指定された地域の天気予報を取得します。",
name: "get_weather",
parameters: %{
properties: %{region: %{description: "The name of the region", type: "string"}},
type: "object"
},
required: ["region"]
}
]
OpenAIのAPIをコールする
OpenAIのAPIに今日の東京の天気をたずねてみましょう。
messages = [
%{role: "user", content: "東京の今日の天気は?"}
]
[%{content: "東京の今日の天気は?", role: "user"}]
先に定義したモジュールを使って呼び出してみます。
{:ok, response} =
OpenAI.chat_completion(
model: "gpt-3.5-turbo-0613",
messages: messages,
functions: functions
)
{:ok,
%{
choices: [
%{
"finish_reason" => "function_call",
"index" => 0,
"message" => %{
"content" => nil,
"function_call" => %{
"arguments" => "{\n \"region\": \"東京\"\n}",
"name" => "get_weather"
},
"role" => "assistant"
}
}
],
created: 1687190705,
id: "chatcmpl-7TBfFEi1L3yUq8xOam1lnx4SbapqK",
model: "gpt-3.5-turbo-0613",
object: "chat.completion",
usage: %{"completion_tokens" => 17, "prompt_tokens" => 77, "total_tokens" => 94}
}}
GPTが知り得ない、今日の天気について質問されているので、期待した通り関数呼び出しをせよというレスポンスが返ってきています。
返り値のfinish_reason
がfunction_call
になっていて、message
にfunction_call
が含まれていると、GPTが関数呼び出しを要求しているということになります。本来は返り値を見てハンドリングする必要がありますが、ここでは簡単のため、関数呼び出しのレスポンスが返ってくる前提でコードを書いていきます。
以下のようにして、メッセージを取り出します。
response_message = response |> get_in([:choices, Access.at(0), "message"])
%{
"content" => nil,
"function_call" => %{"arguments" => "{\n \"region\": \"東京\"\n}", "name" => "get_weather"},
"role" => "assistant"
}
関数の実行
上記の通り、get_weather
という関数を{"region": "東京"}
という引数で実行せよと返ってきたので、対応する関数を呼び出します。その関数を以下のように簡単に定義しておきましょう。
関数の返り値が変なことになっていますが、気にしないでください。実際には、何かしらの外部APIを叩くなどして、引数に応じた天気予報をちゃんと取得することになるでしょう。
funcs = %{
"get_weather" => fn _region ->
"昼過ぎまでは晴れますが夕方以降はカエルが降るでしょう"
end
}
%{"get_weather" => #Function<42.3316493/1 in :erl_eval.expr/6>}
指定された関数名から、対応する関数の実装を取り出します。実際には、この関数は何らかのモジュール内に実装するでしょう。ここでは、簡単のため、Map
内の無名関数として実装しておきます。
%{"name" => function_name, "arguments" => arguments} =
response_message |> Map.get("function_call")
get_weather = funcs |> Map.get(function_name)
#Function<42.3316493/1 in :erl_eval.expr/6>
この関数を実行し、天気予報を取得します。
function_result = get_weather.(arguments |> Jason.decode!())
"昼過ぎまでは晴れますが夕方以降はカエルが降るでしょう"
関数の実行結果をAPIに返す
上記の通り、東京の天気予報を取得できたので、APIに返します。その際、関数呼び出しのレスポンスメッセージと、関数の実行結果とをコンテキストに追加する必要があります。
{:ok, response} =
OpenAI.chat_completion(
model: "gpt-3.5-turbo-0613",
messages:
messages ++
[
response_message,
%{role: "function", name: function_name, content: function_result}
],
functions: functions
)
{:ok,
%{
choices: [
%{
"finish_reason" => "stop",
"index" => 0,
"message" => %{
"content" => "東京の今日の天気は、昼過ぎまでは晴れていますが、夕方以降にはカエルが降る予報です。お出かけの際は傘を持っていくことをおすすめします。",
"role" => "assistant"
}
}
],
created: 1687191939,
id: "chatcmpl-7TBz95peb1zZfvsdr5bnvTDFDyCma",
model: "gpt-3.5-turbo-0613",
object: "chat.completion",
usage: %{"completion_tokens" => 72, "prompt_tokens" => 134, "total_tokens" => 206}
}}
質問した今日の東京の天気について「東京の今日の天気は、昼過ぎまでは晴れていますが、夕方以降にはカエルが降る予報です。お出かけの際は傘を持っていくことをおすすめします。」といういい感じのメッセージにして返してくれました。関数の実行結果がちゃんと使われていますね。GPTさんは、素直で良い子です。
実際には、何度か実行してみると「東京の今日の天気は、昼過ぎまでは晴れですが、夕方以降は雨が降る予報です。ご注意ください。」というような、まともなメッセージが返ってくることもありました。映画「マグノリア」じゃあるまいし、カエルが降ることはないだろうという常識的な判断をするGPTさんの健気な姿に心を打たれます。
おわりに
本記事では、ElixirでOpenAIのAPIの新機能である関数呼び出しを試してみました。これは夢が広がりますね。今後もあれこれ遊んでみたいと思います。
Discussion