OpenAIによるChat APIの新機能Function callingをPythonで使ってみた

[Supership](https://supership.jp/)の名畑です。[Dr.STONE](https://dr-stone.jp/)は分割2クールなのですね。1期、2期も面白かったけれど、今回の3期はさらに面白い。何度涙腺が緩んだことか。さて、10月まで秒数をカウントしますかね……。

## はじめに

[OpenAI](https://openai.com/)（[ChatGPT](https://openai.com/blog/chatgpt)の提供元）による[Chat API](https://platform.openai.com/docs/api-reference/chat)についてまた書かせていただきます。

先日[Function calling and other API updates](https://openai.com/blog/function-calling-and-other-api-updates)にて、APIのアップデートが告知されました。
アップデート内容の一つである**Function calling**(関数呼び出し)が話題になっておりますが、こちらについて私も遅ればせながら試してみたので記事として残します。

**Function calling**における一連の流れを順次説明してまとめていますので、皆様の理解に繋がると嬉しいです。


Chat API自体が初耳という方は過去の記事「[OpenAIのChat API(Chat completions)を用いての会話をPythonで実装してみた記録](https://qiita.com/nabata/items/0919576a926d5bb9814d)」を読んでいただけると嬉しいです。

## そもそもFunction callingって？

先述した[Function calling and other API updates](https://openai.com/blog/function-calling-and-other-api-updates)に説明が書かれています。

> These models have been fine-tuned to both detect when a function needs to be called (depending on the user’s input) and to respond with JSON that adheres to the function signature. Function calling allows developers to more reliably get structured data back from the model. For example, developers can:
> ・ Create chatbots that answer questions by calling external tools (e.g., like ChatGPT Plugins)

Google翻訳にかけると下記

> これらのモデルは、関数を呼び出す必要があるとき (ユーザーの入力に応じて) を検出し、関数の署名に準拠した JSON で応答するように微調整されています。関数呼び出しにより、開発者はモデルから構造化データをより確実に取得できるようになります。たとえば、開発者は次のことができます。
> ・ 外部ツール（ChatGPT プラグインなど）を呼び出して質問に答えるチャットボットを作成する

イメージつきますでしょうか。

要は以下の流れのようです。

1. 関数の定義（名称、役割、引数）をあらかじめ決めておく
1. Chat APIを呼び出す（質問等をする）
1. Chat APIは定義済みの関数を呼び出すべきかどうかを判断する
1. 関数を呼び出すべきと判断した場合、Chat APIは関数名と引数を[JSON](https://json-schema.org/understanding-json-schema/)形式で返してくる
1. 関数のレスポンスをJSON形式で用意する
1. 4と5のJSONを渡してChat APIを再び呼び出す
1. Chat APIは渡されたJSONを利用した回答を行う

**5の段階**で好きな情報を関数のレスポンスとして設定することができます。データベースから引っ張ってきた値を渡すのでも、外部のAPIを呼び出した結果を渡すのでも良いです。要点としては**関数のレスポンスを用意するのはChat APIではなく、実装者**です。
つまりはリアルタイム性のある情報でも良いです。Chat APIのモデルで学習されていないデータを元にしたファインチューニングが実現できます。

ただ、これらは、**Function calling**が提供される前から、プロンプトや実装の工夫によって実現可能ではあります。

**Function calling**の登場とはつまり「**APIのI/F改善によりユーザーがやりたいことをよりやりやすくなった**」ということですかね。

説明だけだとピンとこないと思うので、実装してみます。

## 環境設定

まずはお決まりの環境確認です。
私が使っているのはmacOSです。

Pythonはインストール済み。

```shell-session
$ python --version
Python 3.10.7
```

openaiのライブラリもインストール済みです。

```shell-session
$ openai --version
openai 0.27.6
```

openaiが入っていない人はinstallしておきましょう。

```shell-session
$ pip install openai
```

API Keyは[OpenAI platform](https://platform.openai.com/)で発行済みで、**OPENAI\_API\_KEY**という環境変数名で保存してあるとします。

```bash
export OPENAI_API_KEY=ここに取得したAPI Keyを書く
```

## 関数の定義

今回は **get\_anime\_information** という、アニメの情報を取得する関数を定義してみます。定義だけなので、実装はありません。

```Python
my_functions = [
    {
        "name": "get_anime_information",
        "description": "与えられたアニメタイトルの情報を返します",
        "parameters": {
            "type": "object",
            "properties": {
                "title": {
                    "type": "string", "description": "アニメのタイトル"
                },
            },
            "required": ["title"]
        }
    }
]
```

本当はすべて英語の方が精度が上がるのかもしれませんが、**description**は可読性を考慮して日本語にしました。変数名はわかりやすく**my_functions**とします。

引数は**title**のみです。アニメのタイトルを受け取ります。

関数の定義について詳細は公式ページの[functions](https://platform.openai.com/docs/api-reference/chat/create#chat/create-functions)をご覧ください。
**name**のみが必須です。

## importなど

使用するパッケージのimportとAPI Keyのセットをしておきます。

```Python
import os
import openai
openai.api_key = os.getenv("OPENAI_API_KEY")
```

## 質問を投げてみる

> Developers can now describe functions to gpt-4-0613 and gpt-3.5-turbo-0613, and have the model intelligently choose to output a JSON object containing arguments to call those functions. 

現状では**Function calling**が使えるモデルは**gpt-4-0613**と**gpt-3.5-turbo-0613**です。
ですので**gpt-3.5-turbo-0613**を指定した上で質問を投げます。

質問文は「**TVアニメ「スキップとローファー」について教えてください**」としました。

さきほど定義した**my_functions**も一緒に渡します。

```Python
completion = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[
        {"role": "user", "content": "TVアニメ「スキップとローファー」について教えてください"},
    ],
    functions=my_functions
)
```

すると、下記のレスポンスが**completion**に格納されています。

```Python
{
  "choices": [
    {
      "finish_reason": "function_call",
      "index": 0,
      "message": {
        "content": null,
        "function_call": {
          "arguments": "{\n\"title\": \"スキップとローファー\"\n}",
          "name": "get_anime_information"
        },
        "role": "assistant"
      }
    }
  ],

  〜略〜

}
```

**function\_call**があります。つまり、Chat APIが関数呼び出しをするべきと判断したわけです。私たち実装者は**function\_call**の有無で以降の処理をどうするか分岐することになります。

**function\_call**の中には**get\_anime\_information**という関数名とその引数があります。引数はJSONで下記です。関数の定義に合っていますね。

```Python
{
	"title": "スキップとローファー"
}
```

## 関数レスポンスの用意

関数の回答を用意します。
つまりは「**スキップとローファーという引数を与えられたときの回答を用意する**」ということです。

公式のサンプルコード（天気を答えるfunction）だと、Bostonの天気を聞かれて天気情報取得のAPIを呼び出す設定になっています。

今回は手間をかけないよう、手動で用意しました。
[TVアニメ「スキップとローファー」公式サイト](https://skip-and-loafer.com/)から紹介文を持ってきました。
余談ですが個人的にはかなり好きな作品なので、皆様も是非。胸が熱くなります。

> 地方の小さな中学校から、東京の高偏差値高校に首席入学した岩倉美津未。カンペキな生涯設計を胸に、ひとり上京してきた田舎の神童は、勉強はできるけれど距離感が独特でちょっとズレてる。だから失敗することもあるけれど、その天然っぷりにクラスメイトたちはやわらかに感化されて、十人十色の個性はいつしか重なっていく。知り合って、だんだんわかって、気づけば互いに通じ合う。だれもが経験する心のもやもや、チリチリした気持ち。わかりあえるきっかけをくれるのは、かけがえのない友達。ときどき不協和音スレスレ、だけどいつのまにかハッピーなスクールライフ・コメディ！

こちらをそれっぽくJSONにします。

```Python
{"information": "地方の小さな中学校から、東京の高偏差値高校に首席入学した岩倉美津未。カンペキな生涯設計を胸に、ひとり上京してきた田舎の神童は、勉強はできるけれど距離感が独特でちょっとズレてる。だから失敗することもあるけれど、その天然っぷりにクラスメイトたちはやわらかに感化されて、十人十色の個性はいつしか重なっていく。知り合って、だんだんわかって、気づけば互いに通じ合う。だれもが経験する心のもやもや、チリチリした気持ち。わかりあえるきっかけをくれるのは、かけがえのない友達。ときどき不協和音スレスレ、だけどいつのまにかハッピーなスクールライフ・コメディ！"}
```

## Chat APIにJSONを渡す

では2つのJSONを**assistant**と**function**という**role**で追加した上でAPIを再び呼び出してみます。

```Python
completion = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[
        {"role": "user", "content": "TVアニメ「スキップとローファー」について教えてください"},
        {"role": "assistant", "content": None, "function_call": {"name": "get_anime_information", "arguments": "{\"title\": \"スキップとローファー\"}"}},
        {"role": "function", "name": "get_anime_information", "content": "{\"information\": \"地方の小さな中学校から、東京の高偏差値高校に首席入学した岩倉美津未。カンペキな生涯設計を胸に、ひとり上京してきた田舎の神童は、勉強はできるけれど距離感が独特でちょっとズレてる。だから失敗することもあるけれど、その天然っぷりにクラスメイトたちはやわらかに感化されて、十人十色の個性はいつしか重なっていく。知り合って、だんだんわかって、気づけば互いに通じ合う。だれもが経験する心のもやもや、チリチリした気持ち。わかりあえるきっかけをくれるのは、かけがえのない友達。ときどき不協和音スレスレ、だけどいつのまにかハッピーなスクールライフ・コメディ！\"}"}
    ],
    functions=my_functions
)
print(completion["choices"][0]["message"]["content"])
```

表示されたレスポンスは下記です。

> TVアニメ「スキップとローファー」は、地方の小さな中学校から東京の高偏差値高校に首席入学した岩倉美津未の物語です。岩倉美津未は、勉強はできるけれど距離感が独特でちょっとズレている主人公で、失敗することもありますが、その天然っぷりにクラスメイトたちはやわらかに感化されて、十人十色の個性が重なっていきます。互いに理解し合える友達の存在が、心のもやもややチリチリした気持ちに対する解決策のきっかけを提供します。不協和音スレスレな面もあるけれど、いつの間にかハッピーなスクールライフ・コメディとなります。

渡した内容がしっかり使われていますね。

**Function calling**を用いた実装の一連の流れは以上となります。

## 関数とまったく関係ない質問をすると？

「**東京の観光名所について教えてください**」と聞いてみます。

```Python
completion = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[
        {"role": "user", "content": "東京の観光名所について教えてください"},
    ],
    functions=my_functions
)
print(completion["choices"][0]["message"]["content"])
```

下記のような**functions**を無視した回答が表示されます。つまりは**functions**を設定していないときと同じ振る舞いです。

> 東京には多くの観光名所があります。以下にいくつかの代表的な観光名所をご紹介します。
> 
> 1. 東京ディズニーランド/シー: 東京ディズニーリゾート内にある世界的に有名なテーマパークです。ディズニーキャラクターとの出会いやアトラクション、ショーを楽しむことができます。
>
> 2. 東京スカイツリー: 東京都墨田区にある高さ634メートルの電波塔です。展望台からは360度のパノラマビューが楽しめるほか、レストランやショップもあります。
> 
> 以下略

## 複数の関数を用意すると？

**get\_anime\_information**に加えて新たに**get\_manga\_information**も定義してみました。

```Python
my_functions = [
    {
        "name": "get_anime_information",
        "description": "与えられたアニメタイトルの情報を返します",
        "parameters": {
            "type": "object",
            "properties": {
                "title": {
                    "type": "string", "description": "アニメのタイトル"
                },
            },
            "required": ["title"]
        }
    },
    {
        "name": "get_manga_information",
        "description": "与えられた漫画タイトルの情報を返します",
        "parameters": {
            "type": "object",
            "properties": {
                "title": {
                    "type": "string", "description": "漫画のタイトル"
                },
            },
            "required": ["title"]
        }
    }
]
```

その上で「**漫画「スキップとローファー」について教えてください**」という質問を投げてみます。

```Python
completion = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[
        {"role": "user", "content": "漫画「スキップとローファー」について教えてください"},
    ],
    functions=my_functions
)
print(completion["choices"][0]["message"]["content"])
```

レスポンスは下記です。

```Python
{
  "choices": [
    {
      "finish_reason": "function_call",
      "index": 0,
      "message": {
        "content": null,
        "function_call": {
          "arguments": "{\n\"title\": \"スキップとローファー\"\n}",
          "name": "get_manga_information"
        },
        "role": "assistant"
      }
    }
  ],

  〜略〜

}
```

漫画についての質問ということで**get\_manga\_information**を呼び出すべきだと正しく判定されていますね。

## おまけ

前に「[OpenAI APIでGPT-3にFine-tuningでアニメの放送開始日時を学習してもらうPython実装の記録](https://qiita.com/nabata/items/13962a62a1b298361dcb)」という記事を書きましたが、似たようなことをしてみましょう。
スキップとローファーの放送日時をAPIに答えてもらいます。

まずは**get\_anime\_schedule**という関数の定義をします。**title**でアニメのタイトルを、**channel**で放送局の名称を受け取る関数とします。

```Python
my_functions = [
    {
        "name": "get_anime_schedule",
        "description": "与えられたアニメタイトルの放送曜日と時間を取得します",
        "parameters": {
            "type": "object",
            "properties": {
                "title": {
                    "type": "string", "description": "アニメのタイトル"
                },
                "channel": {
                    "type": "string", "description": "放送局の名称"
                },
            },
            "required": ["title", "channel"]
        }
    },
]
```

「**TVアニメ「スキップとローファー」のTOKYO MXでの放送日時を教えてください**」という質問を投げます。

```Python
completion = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[
        {"role": "user", "content": "TVアニメ「スキップとローファー」のTOKYO MXでの放送日時を教えてください"},
    ],
    functions=my_functions
)
```

下記はレスポンス抜粋です。**title**と**channel**の二つの引数が正しく設定されていますね。

```Python
"function_call": {
  "arguments": "{\n\"title\": \"スキップとローファー\",\n\"channel\": \"TOKYO MX\"\n}",
  "name": "get_anime_schedule"
},
```

回答のJSONを用意します。本来はここで**TOKYO MX**の情報だけを書くのが関数のレスポンスとしては正しいのでしょうが、試しに5局分書いてみました。まあ、TOKYO MXのデータも含まれるから、ギリギリセーフということで。

```Python
 {
    "data":[
        {"channel": "TOKYO MX", "schedule": "火曜日23:00〜"},
        {"channel": "AT-X", "schedule": "水曜日22:00〜"},
        {"channel": "北陸朝日放送", "schedule": "水曜日25:58〜"},
        {"channel": "BS朝日", "schedule": "金曜日23:00〜"},
        {"channel": "関西テレビ放送", "schedule": "日曜日25:59〜"},
    ]
 }
```

2つのJSONを渡して再び呼び出します。

```Python
completion = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[
        {"role": "user", "content": "TVアニメ「スキップとローファー」のTOKYO MXでの放送日時を教えてください"},
        {"role": "assistant", "content": None,
         "function_call": {"name": "get_anime_schedule", "arguments": "{\"title\": \"スキップとローファー\",\"channel\": \"TOKYO MX\"}"}},
        {"role": "function", "name": "get_anime_information",
         "content": """
         {
            "data":[
                {"channel": "TOKYO MX", "schedule": "火曜日23:00〜"},
                {"channel": "AT-X", "schedule": "水曜日22:00〜"},
                {"channel": "北陸朝日放送", "schedule": "水曜日25:58〜"},
                {"channel": "BS朝日", "schedule": "金曜日23:00〜"},
                {"channel": "関西テレビ放送", "schedule": "日曜日25:59〜"},
            ]
         }
         """
        }
    ],
    functions=my_functions
)
print(completion["choices"][0]["message"]["content"])
```

表示されたレスポンスは下記です。

> TVアニメ「スキップとローファー」はTOKYO MXで火曜日23:00に放送されています。

ばっちりTOKYO MXの放送時間だけを返してくれてますね。

**user**の質問文を「**スキップとローファー」のBS朝日での放送日時を教えてください**」に変えてみます。

```Python
{"role": "user", "content": "TVアニメ「スキップとローファー」のBS朝日での放送日時を教えてください"},
```

これに連動して**get\_anime\_schedule**に引数として渡す**channel**も**BS朝日**に変わります。

最終的なレスポンスは下記です。

> TVアニメ「スキップとローファー」はBS朝日で金曜日の23:00〜に放送されています。

正しい回答です。

ちなみに質問を「**TVアニメ「スキップとローファー」の放送日時を教えてください**」として局名を省くとどうなるでしょう。

```Python
{"role": "user", "content": "TVアニメ「スキップとローファー」の放送日時を教えてください"},
```

レスポンスは下記でした。**channel**が空要素になっています。

```Python
"function_call": {
  "arguments": "{\n\"title\": \"\u30b9\u30ad\u30c3\u30d7\u3068\u30ed\u30fc\u30d5\u30a1\u30fc\",\n\"channel\": \"\"\n}",
  "name": "get_anime_schedule"
},
```

これを踏まえて**channel**が空要素のままで呼び出してみました。

```Python
completion = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[
        {"role": "user", "content": "TVアニメ「スキップとローファー」の放送日時を教えてください"},
        {"role": "assistant", "content": None,
         "function_call": {"name": "get_anime_schedule", "arguments": "{\"title\": \"スキップとローファー\",\"channel\": \"\"}"}},
        {"role": "function", "name": "get_anime_information",
         "content": """
         {
            "data":[
                {"channel": "TOKYO MX", "schedule": "火曜日23:00〜"},
                {"channel": "AT-X", "schedule": "水曜日22:00〜"},
                {"channel": "北陸朝日放送", "schedule": "水曜日25:58〜"},
                {"channel": "BS朝日", "schedule": "金曜日23:00〜"},
                {"channel": "関西テレビ放送", "schedule": "日曜日25:59〜"},
            ]
         }
         """
        }
    ],
    functions=my_functions
)
```

レスポンスは以下です。箇条書き部分は実際にはMarkdown形式(行頭が-)でした。

> TVアニメ「スキップとローファー」の放送日時は以下の通りです：
> 
> - TOKYO MX：火曜日23:00〜
> - AT-X：水曜日22:00〜
> - 北陸朝日放送：水曜日25:58〜
> - BS朝日：金曜日23:00〜
> - 関西テレビ放送：日曜日25:59〜
> 
> ご参考までにどうぞ。

5局全部を表示してくれました。

**Function calling**を用いなくても実現できる内容ではありましたが、まあ、おまけということで。

## 最後に

変化が激しくて楽しい。

## 最後に2

記事の題材としてなぜ冒頭の**Dr.STONE**を選ばなかったのかというと、**Dr.STONE**のTVアニメ1期は2019年に放映されているため、学習データにすでになにかしら含まれている可能性があるかもしれないと考えたからです。[GPT-3.5](https://platform.openai.com/docs/models/gpt-3-5)の説明を見ると**gpt-3.5-turbo-0613**の**TRAINING DATA**は**Up to Sep 2021**のため。

## 宣伝

[SupershipのQiita Organization](https://qiita.com/organizations/supership-inc)を合わせてご覧いただけますと嬉しいです。他のメンバーの記事も多数あります。

Supershipではプロダクト開発やサービス開発に関わる方を絶賛募集しております。
興味がある方は[Supership株式会社 採用サイト](https://supership.jp/recruit/)よりご確認ください。