ChatGPT APIのFunction callingを使って頑張らずにチャットをアクションに繋げる


# はじめに

2023/6/13のOpenAIのアップデートで`Function calling`という機能が追加されました。

https://openai.com/blog/function-calling-and-other-api-updates

触ってみたところ今まで気合いと若干の運頼みでどうにかしていたところを綺麗にできる機能なのが分かったので試してみました。

個人的には`Function calling`という名前がちょっと紛らわしかったです。`Function selecting`みたいな感じじゃないかと思いました。

やってくれることは、雑にいうとユーザーの入力メッセージに対して使えそうな関数を選んでレスポンスしてくれるというものです。これだけだと有り難みが分かりにくいので説明して行きます。

# ChatGPT APIのレスポンスを何らかのアクションに繋げるには何が必要だったのか

この機能ができるまでChatGPT APIのレスポンスから何かしらアクションを行わせたい時、以下の実装が必要でした。

**1. レスポンス内容の指定**
**2. レスポンスのパース**
**3. パース結果を関数に渡す**

まず、ChatGPT APIに何か質問したとして返ってくるのはテキストメッセージです。（**Chat**GPTなので当たり前ですが）

なので例えばJSON形式など、その後の工程で使いやすい形で返すよう、ChatGPT APIに指示を出していました。
一例ですが、こんな感じのプロンプトを書いたりしてました。
```python
role = "
Strictly adhere to the following restrictions.
- You are a pharmacist.
- You are professional, but also kind, friendly, and respectful.
- You reply in the user's language.
- You can reply only in the JSON format including the \"message\" and \"medicines\" properties.
- If medicine is recomended, include each in this property in array format. Array elements must be names only, not sentences.
- if medicine is not recomended, set \"medicines\" value to null.
- Again, you must in any case respond in JSON format which has the message and medicines parameters.
"
```

トークン節約のため英語にしてますが、内容としては薬剤師という設定で、
1. 絶対にJSONで答えろ
2. そのJSONには`message`と`medicines`というプロパティを含めろ
3. オススメの薬があったらmedicinesに配列で含めろ
4. オススメの薬がないならmedicinesは`null`にしろ
5. 繰り返す、絶対JSONで返せ！

みたいなことが書いてあります。細かいことは置いておいて、要はレスポンス内容を指定しようとしているということです。

ChatGPT APIからのレスポンスをその後に別の処理で使いたければ、パースしやすいようにしてどんな値を返すかまで指示しないといけなかったということですね。そうしないとただのテキストで返ってくるので正規表現で頑張る手もあるかもですが中々大変かなと思います。

ちなみに`gpt-3.5-turbo`ではここまで書いてもJSONで返ってこないことが結構な確率でありました。
なのでこの後にパース失敗時やパースできても想定してるプロパティが入ってない時などに対応できるような処理を書き、ユーザーにはそれっぽく答えをするようにしてました。運が絡むものでした。


# Fanction callingで何ができるようになったのか

プログラムの例があったほうが早いので書いてみます。
例なので単純なものにしています。また、より汎用的にするためこの例ではOpenAIのモジュールを使っていません。

全体像は以下ですが、下に部分ごとに解説を書きます。
```python
import requests
import json

def search_medicine(medicine):
    url = "https://www.amazon.co.jp/s?k=" + medicine
    return url

def ask_chat_gpt():
    headers = {
        "Authorization": "Bearer your-token"
    }
    messages = [
        {
            "role": "system",
            "content": "あなたは薬剤師です。ユーザーの症状に対してオススメの薬の名前を具体的に教えてください。"
        },
        {
            "role": "user",
            "content": "微熱があるみたい"
        }
    ]
    functions = [
        {
            "name": "search_medicine",
            "description": "症状に合わせた推奨される医薬品の候補を返す",
            "parameters": {
                "type": "object",
                "properties": {
                    "medicine": {
                        "type": "string",
                        "description": "薬の名前"
                    }
                },
                "required": ["medicine"]
            }
        }    
    ]
    function_call = "auto"
    body = {
        "model": "gpt-3.5-turbo-0613",
        "messages": messages,
        "functions": functions,
        "function_call": function_call
    }
    response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=body)
    return response.json()

if __name__ == '__main__':
    response = ask_chat_gpt()
    print(response)
```

ChatGPT APIへのリクエストはこんな形のBodyになります。
```python
body = {
    "model": "gpt-3.5-turbo-0613", # モデル
    "messages": messages, # 入力するメッセージ
    "functions": functions, # ChatGPT APIに伝える関数群
    "function_call": "auto" # autoにしておくと、functionsで伝えた関数から使えそうなものを選ぶ
}
```

次に、大事なのがこの部分です。`search_medicine`1つだけしか書いてないですが、ここにAPI側に伝える関数を書いて行きます。関数名、その説明、引数ですね。
```python
functions = [
    {
        "name": "search_medicine",
        "description": "症状に合わせた推奨される医薬品の候補を返す",
        "parameters": {
            "type": "object",
            "properties": {
                "medicine": {
                    "type": "string",
                    "description": "薬の名前"
                }
            },
            "required": ["medicine"]
        }
    }    
]
```

実行してみると以下のような内容になります。
```json
{
  "id": "chatcmpl-7SsfdsfTDGE6TSd02XWL",
  "object": "chat.completion",
  "created": 1687022283,
  "model": "gpt-3.5-turbo-0613",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,
        "function_call": {
          "name": "search_medicine",
          "arguments": {
            "medicine": "解熱剤"
          }
        }
      },
      "finish_reason": "function_call"
    }
  ],
  "usage": {
    "prompt_tokens": 139,
    "completion_tokens": 21,
    "total_tokens": 160
  }
}
```

`message`の中に`function_call`とあり、search_medicineが選択されてます。また、`arguments`に引数で`medicine`が`解熱剤`と入ってます。入力メッセージと関数の説明から、この関数を選択したわけですね。
```json
{
  "index": 0,
  "message": {
    "role": "assistant",
    "content": null,
    "function_call": {
      "name": "search_medicine",
      "arguments": {
        "medicine": "解熱剤"
      }
    }
  },
  "finish_reason": "function_call"
}
```

あとは、この結果から好きに処理を書けば良いです。
```python
function = message["function_call"]["name"]
if function == "search_medicine":
    medicine = json.loads(message["function_call"]["arguments"])["medicine"]
    medicine_url = search_medicine(medicine)
    print(medicine_url)
    ..その他好きな処理..
```
実際には必ずしも症状を含むコメントではない入力がされることもあるので、
```python
if message.get("function_call"):
    function = message["function_call"]["name"]
    if function == "search_medicine":
        medicine = json.loads(message["function_call"]["arguments"])["medicine"]
        medicine_url = search_medicine(medicine)
        print(medicine_url)
else:
    print(message["content"])
```
↑こんな感じにすればそれにも対応できます。ChatGPT APIに伝えている関数に、該当するものがなかった場合の処理です。

単に`こんにちは`とやってみると、
```python
    messages = [
        {
            "role": "system",
            "content": "あなたは薬剤師です。プロフェッショナルな薬剤師として振る舞ってください。"
        },
        {
            "role": "user",
            # "content": "微熱があるみたい"
            "content": "こんにちは"
        }
    ]
```
普通にコメントが返ってきます。
```
こんにちは！どのようなお困りごとがありますか？お手伝いできることがあれば教えてください。
```

# まとめ
`Function calling`を使えば、ユーザーのメッセージに対してアクションに繋げるプログラムを簡単に作れます。
今までもできなくはなかったですが、かなりやりやすくなったなと思います。頑張ってプロンプトでレスポンスの形式を指定しなくて良くなりましたし、複数の関数を伝えておけばユーザー入力に対して最適な次の処理を選んでくれます。

だいぶ応用しやすくなったと感じます。

# 参考

- Function calling 

https://platform.openai.com/docs/guides/gpt/function-calling