OpenAIのFunction CallingとグルメサーチAPIを連携してオススメ店を紹介させてみた

## はじめに
6/13にOpenAIは新しい機能として「Function calling」を発表しました。Function callingは外部APIやシステムの関数の呼び出しをチャットなどユーザの入力から判断して実行、連携する事が出来る仕組みです。これまでLangChainなどのライブラリを用いてGoogleブラウジングなどの外部サービスと連携などを行っておりましたが、Function callingの機能により直接APIと連携させることが容易になり応用の幅がより一掃広くなりました。

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

:::note info
Function calling活用例
- 外部ツールを呼び出して質問に答えるチャットボットを作成
- 自然言語を API 呼び出しまたはデータベース クエリに変換
- テキストから構造化データを抽出
:::

今回はOpenAIの新しい機能であるFunction callingの一端であるAPIの連携による回答を検証してみました。
以下の記事で、ユーザからのロケーション情報を元に緯度経度をOpenWeatherMapのAPIに渡して実際の天気情報を返す事例が紹介されていたことから、ロケーション情報を渡せる別のAPIであるリクルートが提供するグルメサーチAPIで試してみようと思いました。

■ 参考

https://qiita.com/SatoshiGachiFujimoto/items/f55b4187c9f681fe069a

## 前準備
### APIキーの取得
以下より手順に従ってAPIキーを取得します。

https://webservice.recruit.co.jp/register/

■ ホットペッパーグルメサーチAPIリファレンス

https://webservice.recruit.co.jp/doc/hotpepper/reference.html#1

## 検証

- オススメ店情報の取得

```python
def get_recommended_restaurant_info(location):
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[
            {
                "role": "system",
                "content": 'JSONフォーマットで答えてください。{"latitude": {float number}, "longitude": {float number}}',
            },
            {
                "role": "user",
                "content": f"{location}の緯度経度は？",
            },
        ],
    )

    message = response["choices"][0]["message"]
    json_data = json.loads(message["content"])
    latitude = json_data["latitude"]
    longitude = json_data["longitude"]

    url = f"https://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key={RECRUIT_API_KEY}&lat={latitude}&lng={longitude}&range=2&order=4&count=1&format=json"

    response = requests.get(url)
    data = response.json()

    recommended_restaurant = data["results"]["shop"][0]

    restaurant_info = {
        "name": recommended_restaurant["name"],
        "address": recommended_restaurant["address"],
        "genre": recommended_restaurant["genre"]["name"],
        "budget": recommended_restaurant["budget"]["average"],
        "access": recommended_restaurant["access"],
    }
    return json.dumps(restaurant_info)
```

元記事を参考に指定されたlocationに基づいてレストラン情報を取得する処理を用意します。OpenAIの用いて対話型コンテキストを生成して、ユーザに対してJSON形式で緯度経度情報をレスポンスするように要求します。

その後、ユーザから受け取ったlocationJSONから緯度経度を抽出してURLにパラメータとして適用してAPIを実行させます。

|パラメータ|説明|
|---|---|
|key|APIキー|
|lat|ある地点からの範囲内のお店の検索を行う場合の緯度|
|lng|ある地点からの範囲内のお店の検索を行う場合の経度|
|range|ある地点からの範囲内のお店の検索を行う場合の範囲を5段階で指定。<br> 1.300m <br>2.500m <br>3.1000m<br>4.2000m<br>5.3000m|
|order|検索結果の並び順を指定。<br> 1.店名かな順 <br>2.ジャンルコード順 <br>3.少エリアコード順<br>4.おすすめ順|
|count|検索結果の最大出力データ数を指定|
|format|レスポンスをXMLかJSONかJSONPかを指定|

その後、APIのレスポンスデータから必要な情報（店名、住所、ジャンル、緯度、経度、予算、アクセス情報など）を抽出して格納、jsonとして返却させます。

- restaurant_infoの中身の例
ググるとちゃんと出てくるお店である事が分かります。

```json
{
  "name": "ひかり 上野駅前店",
  "address": "東京都台東区上野２-14-31 レイクサイドビル　８F",
  "genre": "居酒屋",
  "budget": "通常時：3000円　宴会時：3500円",
  "access": "【上野駅不忍口3分・京成上野駅から1分】新感覚の野菜巻き＆レモンサワーを楽しむ個室居酒屋◎手ごろに楽しむ和食宴会♪"
}
```

https://www.hotpepper.jp/strJ001275314/?vos=evhpppg0007&pog=mt()ti(dsa-1345474687092)dv(c)cr(655886355587)fi()gi(126985191307)ci(11562143670)lc(1009300)ps()nw(g)&gclid=Cj0KCQjw1rqkBhCTARIsAAHz7K3e7aQVdTtGTVJ_xGQ1c8AhZiQAM78zlz2zF7eVc5kWjRvUo1qLCkYaAiJ2EALw_wcB&gclsrc=aw.ds

- メイン

```python
def run_chat_model():
    if len(sys.argv) > 1:
        location = sys.argv[1]
    else:
        location = input("場所を入力してください: ")

    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[
            {
                "role": "system",
                "content": "日本語で正確にお店の名前と住所、予算などを紹介してください。"
            },
            {
                "role": "user",
                "content": f"{location}のオススメのお店は？"
            },
        ],
        functions=[
            {
                "name": "get_recommended_restaurant_info",
                "description": "指定された場所に基づいて推奨されるレストランを取得",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "location": {
                            "type": "string",
                            "description": "都市と地域を指定。例：東京、日本",
                        },
                        "genre": {
                            "type": "string",
                            "description": "レストランのジャンルを指定。例：イタリアン、寿司",
                        },
                    },
                    "required": ["location"],
                },
            }
        ],
        function_call="auto",
    )

    message = response["choices"][0]["message"]
    print(message)

    if message.get("function_call"):
        function_name = message["function_call"]["name"]
        arguments = json.loads(message["function_call"]["arguments"])
        print(arguments)

        if arguments["location"]:
            function_response = get_recommended_restaurant_info(
                location=arguments["location"],
             )

            second_response = openai.ChatCompletion.create(
                model="gpt-3.5-turbo-0613",
                messages=[
                    {
                        "role": "system",
                        "content": "日本語で正確にお店の名前と住所、予算などを楽しそうに紹介してください。",
                    },
                    {"role": "user", "content": f"{location}の近くでオススメのお店は？"},
                    message,
                    {
                        "role": "function",
                        "name": function_name,
                        "content": function_response,
                    },
                ],
            )

            message = second_response["choices"][0]["message"]
            print(message["content"])
            return second_response

if __name__ == "__main__":
    run_chat_model()
```

検証なのでチャットボットを用意はせずスクリプト実行時にlocation情報を引数で渡す形式にしています。

- functions Calling部分

functions部分ではモデルがJSON入力を生成する可能性のあるリストを定義していきます。※Function callingでは定義された関数を、対話の中で使用して、指定されたパラメータを引数として受け取ります。それらに応じて関数の処理が実行され、関数の戻り値が会話の中で利用されます。

下記の場合、parameters内でlocationとgenreの2つのパラメータが定義され、各パラメータにはtype（データ型）、description（説明）、required（必須かどうか）などをプロパティとして記述しています。

|functionsの要素|説明|
|---|---|
|name|呼び出される関数の名前。※必須|
|description|動作の説明|
|parameters|関数が受け入れるパラメータをJSONスキーマオブジェクトとして定義|

```
        functions=[
            {
                "name": "get_restaurant_info",
                "description": "Get the recommended restaurants based on location",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "location": {
                            "type": "string",
                            "description": "都市と地域を指定。例：東京、日本",
                        },
                        "genre": {
                            "type": "string",
                            "description": "レストランのジャンルを指定。例：イタリアン、寿司",
                        },
                    },
                    "required": ["location"],
                },
            }
        ],
        function_call="auto",
```

- コード全体
```openai_function_calling_sample.py
import os
import sys
import openai
import json
import requests
from dotenv import load_dotenv

load_dotenv()

OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
RECRUIT_API_KEY = os.environ.get("RECRUIT_API_KEY")

def get_recommended_restaurant_info(location):
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[
            {
                "role": "system",
                "content": 'JSONフォーマットで答えてください。{"latitude": {float number}, "longitude": {float number}}',
            },
            {
                "role": "user",
                "content": f"{location}の緯度経度は？",
            },
        ],
    )

    message = response["choices"][0]["message"]
    json_data = json.loads(message["content"])
    latitude = json_data["latitude"]
    longitude = json_data["longitude"]

    url = f"https://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key={RECRUIT_API_KEY}&lat={latitude}&lng={longitude}&range=2&order=4&count=1&format=json"

    response = requests.get(url)
    data = response.json()

    recommended_restaurant = data["results"]["shop"][0]

    restaurant_info = {
        "name": recommended_restaurant["name"],
        "address": recommended_restaurant["address"],
        "genre": recommended_restaurant["genre"]["name"],
        "budget": recommended_restaurant["budget"]["average"],
        "access": recommended_restaurant["access"],
    }
    print(restaurant_info)
    return json.dumps(restaurant_info)

def run_chat_model():
    if len(sys.argv) > 1:
        location = sys.argv[1]
    else:
        location = input("場所を入力してください: ")

    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[
            {
                "role": "system",
                "content": "日本語で正確にお店の名前と住所、予算などを紹介してください。"
            },
            {
                "role": "user",
                "content": f"{location}のオススメのお店は？"
            },
        ],
        functions=[
            {
                "name": "get_recommended_restaurant_info",
                "description": "指定された場所に基づいて推奨されるレストランを取得",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "location": {
                            "type": "string",
                            "description": "都市と地域を指定。例：東京、日本",
                        },
                        "genre": {
                            "type": "string",
                            "description": "レストランのジャンルを指定。例：イタリアン、寿司",
                        },
                    },
                    "required": ["location"],
                },
            }
        ],
        function_call="auto",
    )

    message = response["choices"][0]["message"]
    print(message)

    if message.get("function_call"):
        function_name = message["function_call"]["name"]
        arguments = json.loads(message["function_call"]["arguments"])
        print(arguments)

        if arguments["location"]:
            function_response = get_recommended_restaurant_info(
                location=arguments["location"],
             )

            second_response = openai.ChatCompletion.create(
                model="gpt-3.5-turbo-0613",
                messages=[
                    {
                        "role": "system",
                        "content": "日本語で正確にお店の名前と住所、予算などを楽しそうに紹介してください。",
                    },
                    {"role": "user", "content": f"{location}の近くでオススメのお店は？"},
                    message,
                    {
                        "role": "function",
                        "name": function_name,
                        "content": function_response,
                    },
                ],
            )

            message = second_response["choices"][0]["message"]
            print(message["content"])
            return second_response

if __name__ == "__main__":
    run_chat_model()
```

- 実行結果
引数に地名を渡して実行

```sh
python3 openai_function_calling.py 上野
```

OpenAIのChatCompletionを使いつつ、グルメサーチAPIによってオススメされたお店を返してもらうことが出来ました!通常のOpenAIやChatGPTで「上野」と打ったところで地名の案内がされるだけなので挙動の違いが分かるとか思います。

```
{
  "content": null,
  "function_call": {
    "arguments": "{\n  \"location\": \"\u4e0a\u91ce\"\n}",
    "name": "get_restaurant"
  },
  "role": "assistant"
}
{'location': '上野'}
上野の近くには「地鶏の王様 上野店」というお店があります。
住所は東京都台東区上野6丁目4-14です。
ジャンルは居酒屋で、予算は3000円（食べ飲み放題コース3000円から！黒毛和牛も食べ放題！）です。
アクセスはJR上野駅徒歩3分/JP御徒町駅徒歩2分です。【焼き鳥と野菜串焼きと餃子が旨い店　完全個室　地鶏の王様】
```

出てきたお店

https://www.hotpepper.jp/strJ001259429/

## 複数のオススメ店舗取得
１店舗だけは物足りないのでもう少し多くのオススメ店舗を紹介してもらうべく、修正を加えます。

- randomモジュールのインポートとget_recommended_restaurant_info関数を以下のように変更して、10種類のオススメ店舗からランダムで3つを紹介させます。


```python
import random

-----

def get_recommended_restaurant_info(location):
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[
            {
                "role": "system",
                "content": 'JSONフォーマットで答えてください。{"latitude": {float number}, "longitude": {float number}}',
            },
            {
                "role": "user",
                "content": f"{location}の緯度経度は？",
            },
        ],
    )

    message = response["choices"][0]["message"]
    json_data = json.loads(message["content"])
    latitude = json_data["latitude"]
    longitude = json_data["longitude"]

    url = f"https://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key={RECRUIT_API_KEY}&lat={latitude}&lng={longitude}&range=2&order=4&count=10&format=json"

    recommended_restaurants = requests.get(url).json()["results"]["shop"]

    restaurant_infos = []
    for restaurant in recommended_restaurants:
        info = {
            "name": restaurant["name"],
            "address": restaurant["address"],
            "genre": restaurant["genre"]["name"],
            "budget": restaurant["budget"]["average"],
            "access": restaurant["access"],
        }
        restaurant_infos.append(info)

    selected_restaurants = random.sample(restaurant_infos, 3)

    print(selected_restaurants)
    return json.dumps(selected_restaurants)
```

- 実行結果

引数に地名を渡して実行
```sh
python3 openai_function_calling.py 博多
```

```
{
  "content": null,
  "function_call": {
    "arguments": "{\n  \"location\": \"\u535a\u591a\"\n}",
    "name": "get_recommended_restaurant_info"
  },
  "role": "assistant"
}
{'location': '博多'}
博多のおすすめのお店を3軒紹介します！

1. 韓国料理食べ放題 MEGUMEGU 天神店
   住所：福岡県福岡市中央区天神2-7-16 DADAビルB1F-B
   ジャンル：韓国料理
   アクセス：西鉄天神大牟田線西鉄福岡（天神）駅北口より徒歩4分/福岡市地下鉄空港線天神駅2出口より徒歩5分

2. WARAI 和楽屋 天神店
   住所：福岡県福岡市中央区天神2-7-16 DADAビルB1F-A
   ジャンル：居酒屋
   予算：2980円★最低150種食べ飲み放題が2480円〜！地域最安値でご提案
   アクセス：西鉄天神大牟田線西鉄福岡（天神）駅北口より徒歩4分/福岡市地下鉄空港線天神駅2出口より徒歩5分

3. イタリアンダイニング YUMMYGARDEN 天神今泉店
   住所：福岡県福岡市中央区今泉1-11-5 ロクラス今泉2F
   ジャンル：ダイニングバー・バル
   予算：3300円〜3800円〜4000円〜5000円【キャッシュレス5％還元対象店舗】
   アクセス：高級食材を有名ホテル出身シェフチームが料理。全て食べ飲み放題でも楽しいです。/西鉄福岡駅から徒歩4分

どのお店も美味しい料理を提供しており、リーズナブルな価格帯も魅力です。ぜひ訪れて、楽しい食体験をしてください！
```

複数のオススメ店舗を紹介させる事が出来ました！

## 課題
今回、先ずは手触りを知る検証としてましたが、何度か実行した際に「目黒」を「眼黒」と店名が微妙に間違っていたり、「銀邸」なのにChatCompletionでは「鉄鍋」と返されたりとややズレが見受けられたので、使いこなすには細かな調整や追加の調査が必要そうな所感でした。

## おわりに
Function callingは外部のAPIと連携するだけではなく、会話の中でユーザが渡した情報から関数Aを実行するのか、関数Bを実行するか判断して処理が出来たり、Aの後にBを実行なども書き方によっては可能ですので、今後は複数関数のを使い分けさせる複雑なパターンで試してみたいですね。

作り込めば、ChatGPTのUI上でPluginでやっていたこと、同じ様な事が独自のアプリケーション上で具体的に実行させることが容易に出来る様になるので大きな可能性秘めていると思います。

また、これまで学習などによって独自データを参照させていたものが、APIを介することでチャットに回答させる事が出来る様になったので導入のハードルも下がったかもしれません。引き続き検証して何が出来て出来ないのかを把握に努めていきます。

## 参照

https://note.com/npaka/n/n917463f55b8a

https://zenn.dev/kazuwombat/articles/1f39f003298028

https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb