OpenAI #ChatGPT API におけるFunction calling実装の面白さについて

https://qiita.com/geeorgey/items/124346b9f4762f7f0e4b

ChatGPTをSlackから使いたい！ということでこちらのSlackアプリを作っているのですが、先日発表されたFunction callingについての実装を行ってみてその面白さについて書いていきたいと思います。

## なにをやったのか
https://twitter.com/geeorgey/status/1672159236103733250

Salesforce連携を行うプロセスで、Function callingを利用しようと思った訳です。
というより、これがなければ連携してもうまく使えないと思っていました。

## Salesforce連携が難しいと思った理由
SalesforceのREST APIを利用する場合、レコードの取得にはSOQLといって、データベースにクエリを投げる必要があります。
例えば、取引先のレコードを取得する場合は
SELECT ID, Name From Account WHERE Name = '株式会社リバネス';
という形にする必要があります。
ChatGPTから利用する場合でFunction callingが無いと、これがかなりしんどいんですよね。

## Function callingがなぜ福音となるのか
例えばSlackから
リバネスの今週アップデートのあった商談を教えて
という質問をしたとします。
Function calling がない場合にこれを処理するというのは、選択肢がほぼありませんでした。特にSlackで使うとなると不可能な実装なのです。
いろんなテキストに対してレスポンスを返す必要がありますからね。単一昨日として提供するというのであれば出来るのですが、そうではない場合に現実的な実装がありませんでした。

Function callingを使うと何が良いかと言うと、入力された文言から必要な処理を探して勝手に使ってくれるという部分です。
Function callingを使う場合は、こんな風に書きます。

```python
    function_call_text = "auto"
    if is_use_function_call == False:
        function_call_text = "none"
    elif use_function_name is not None:
        function_call_text = {
            "name": use_function_name
        }
    try:
        if model == "gpt-3.5-turbo" or model == "gpt-3.5-turbo-0301":
            model = "gpt-3.5-turbo-0613"
        elif model == "gpt-4" or model == "gpt-4-0314":
            model = "gpt-4-0613"
        response = openai.ChatCompletion.create(
            model=model,
            messages=[
                {"role": "user", "content": text},
            ],
            functions=functions,
            function_call=function_call_text,
        )
    except Exception as e:
        logger.debug(e)
```

ChatCompletion.createの引数にfunctionsとfunction_callというものが追加されています。
これらを使う場合は、モデルが0613以降である必要があるので注意してください。(今後、0613がマージされたタイミングでは気にする必要がなくなります)
functionsという部分に、使わせたいfunctionを喰わせるという使い方になります。
ちなみにfunction_callに固定のfunction名を指定すると、必ずそれを使ってくれます。

以下は、今回Salesforceの更新商談の取得に使ったfunctionです。

```python
    text_function_salesforce_latest_opportunities = """
    取引先名に紐づく特定の期間に更新のあった商談情報を取得する
    """
    function_salesforce_latest_opportunities = {
        "name": "get_salesforce_latest_opportunities_of_the_account",
        "description": text_function_salesforce_latest_opportunities,
        "parameters": {
            "type": "object",
            "properties": {
                "account_name": {
                    "type": "string",
                    "description": "取引先名",
                },
                "from_datetime":{
                    "type": "string",
                    "description": "テキストの中の情報から現在の日時を基準として、期間の最初の日時をDatetime型で指定。「最近」は一週間前から今日までとする。現在の日時は" + str(dt.now(timezone('utc'))),
                },
                "to_datetime":{
                    "type": "string",
                    "description": "テキストの中の情報から現在の日時を基準として、期間の最後の日時をDatetime型で指定。現在の日時は" + str(dt.now(timezone('utc'))),
                },
                "time_specified_type":{
                    "type": "string",
                    "description": "テキストの中から日時の指定方法を判断する。単一の日付を指定する場合は single_date、日付が複数にまたがる場合は multiple_datesを返す。「最近の」「この1週間」等の指定はmultiple_dates。「2023年5月1日」や「今日」「昨日」「10日前」という指定はsingle_dateを返す",
                }
            },
            "required": ["account_name", "from_datetime", "to_datetime", "time_specified_type"],
        },
    }    
```

ここからがプロンプトエンジニアリングの面白い部分になります

まず、最初にChatCompletion.createが呼ばれたときにChatGPTが何をするかと言うと、functionsのdescriptionを読みに行きます。入力されたテキストの中から、該当しそうなfunctionの有無を判断します。
なので、このdescriptionの書き方で、使われるか否かが決まるので重要です。

利用されるFunctionが決まったあとは、parameters>propertiesの中に設定した項目に対してChatGPTが答えを出そうとします。


例えばaccount_nameについては、テキストの中から取引先名を抽出してくれます。
先程の例である「リバネスの今週アップデートのあった商談を教えて」
であれば、"リバネス"が抽出されます。
from_datetimeとto_datetimeには何が入るかと言うと「今週」を解釈して日付を入れてくれます。
to_datetimeのdescriptionには"現在の日時は"という形で現在の日時情報を入れてあります。これを使って相対的な時間をChatGPTが設定してくれるというものです。


```python
                message = response["choices"][0]["message"]
                if message.get("function_call"):
                    function_name = message["function_call"]["name"]
                    # Function callの結果を取り出す部分
                    arguments=json.loads(message["function_call"]["arguments"])
                    if function_name == "get_salesforce_latest_opportunities_of_the_account":

                        function_response = get_salesforce_latest_opportunities_of_the_account(
                            installation = my_installations[0],
                            account_name = arguments.get("account_name"),
                            from_datetime = arguments.get("from_datetime"),
                            to_datetime = arguments.get("to_datetime"),
                            time_specified_type = arguments.get("time_specified_type"),
                            time_zone = time_zone
                        )
```

そんな形でレスポンスを取得していきます。
response["choices"][0]["message"]の中にfunction_callというものがあれば、先程設定したfunctionsの中のどれかが利用されたということです。
message["function_call"]["arguments"]の中に、定義した情報が格納されていますので、これを使って処理をすすめてください。
function_responseという形式でJSON文字列を取得したらそれをもう一度ChatGPTに含めて投げるのです。
そうすることで、最初の質問に必要な情報を踏まえた返事をしてくれるようになります。

function_responseには自作の関数で作ったJSONを入れることが出来るのですが、ここにも指示を追加出来るような印象があります。

```python
    return_json = {
        "命令": text_function_salesforce_latest_opportunities,
        "現在の日時": str(dt.now(timezone('utc'))),
        "取引先名": account_name
    }
```
こんな感じで命令項目を追加して指示をしてみたところ、それっぽい挙動になりました。

## まとめ
- Function calling面白い
- 指示を出せる部分が3段階くらい存在する
    - functionのdescription
    - function内のpropertiesに追加した項目のdescription
    - function_responseで取得するJSONの中身

これはプロンプトエンジニアリングってやつだなと非常に感動しました。
通常のプログラミング脳とは違う脳みそを使ってる感じはするのですが、ChatGPTを会話をするように実装していくというのも面白いものですね。