ChatGPTの新機能！Function callingでかわいいお天気お姉さんつくーる

# はじめに

ChatGPTの新機能！Function callingを使って、かわいいお天気お姉さんを作りました！

Function callingのサンプルコードはここにあります。
https://platform.openai.com/docs/guides/gpt/function-calling

# 開発環境
- Windows 10 PC
- Python 3.10
- OpenAI


# 導入

1．OpenAIのAPIキーを取得

https://platform.openai.com/account/api-keys

2．One Call API 2.5のAPIキーを取得

https://home.openweathermap.org/api_keys

参考まで

https://dev.classmethod.jp/articles/use-open-weather-map/

3．1回目のリクエストでユーザーの質問文からlocationを取ってきます。

~~~python
response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[
            {"role": "system", "content": "あなたはお天気お姉さんです。日本語で天気の説明をかわいい感じでお願いします。"},
            {"role": "user", "content": "熊本市の新市街の天気は？"},
        ],
        functions=[
            {
                "name": "get_current_weather",
                "description": "Get the current weather in a given location",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "location": {
                            "type": "string",
                            "description": "The city and state, e.g. San Francisco, CA",
                        },
                    },
                    "required": ["location"],
                },
            }
        ],
        function_call="auto",
    )

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

function_call->argumentsの中にlocationが取り出せています！
{'location': '熊本市'}

~~~json
{
  "content": null,
  "function_call": {
    "arguments": "{\n  \"location\": \"\u718a\u672c\u5e02\"\n}",
    "name": "get_current_weather"
  },
  "role": "assistant"
}
~~~

4．このlocationを自作の関数get_current_weatherに渡します

~~~python
    # Step 2, check if the model wants to call a function
    if message.get("function_call"):
        function_name = message["function_call"]["name"]
        arguments = json.loads(message["function_call"]["arguments"])
        print(arguments)

        if arguments["location"]:
            # Step 3, call the function
            # Note: the JSON response from the model may not be valid JSON
            function_response = get_current_weather(
                location=arguments["location"],
            )
~~~

5．get_current_weatherでは、ChatGPTにlocationの緯度経度をJSONフォーマットで答えてもらい、その緯度経度からOne Call APIを用いてリアルタイムの天気情報を取得します。

~~~python 
# Example dummy function hard coded to return the same weather
# In production, this could be your backend API or an external API
def get_current_weather(location):
    print(location)

    # TODO: call your backend API or external API here
    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"]

    # APIエンドポイントURLを構築
    url = f"https://api.openweathermap.org/data/2.5/onecall?lat={latitude}&lon={longitude}&units=metric&lang=ja&appid={weather_api_key}"

    # APIリクエストを送信してレスポンスを取得
    response = requests.get(url)

    # レスポンスのJSONデータを取得
    data = response.json()
    # print(data)

    # 必要な情報を抽出して表示（例：現在の気温と天気の説明）
    current_weather = data["current"]
    # 'current': {'dt': 1686744295, 'sunrise': 1686686918, 'sunset': 1686738364, 'temp': 22.76, 'feels_like': 23.39, 'pressure': 1007, 'humidity': 88, 'dew_point': 20.67, 'uvi': 0, 'clouds': 100, 'visibility': 10000, 'wind_speed': 1.02, 'wind_deg': 121, 'wind_gust': 1.22, 'weather': [{'id': 804, 'main': 'Clouds', 'description': '厚い雲', 'icon': '04n'}]}

    """Get the current weather in a given location"""
    weather_info = {
        "location": location,
        "temperature": current_weather["temp"],
        "weather_description": current_weather["weather"][0]["description"],
        "longitude": longitude,
        "latitude": latitude,
    }
    return json.dumps(weather_info)
~~~

6．天気情報を取得したら、2回目のリクエストでかわいいお天気お姉さんの回答が返ってきます！

~~~python
            # Step 4, send model the info on the function call and function response
            second_response = openai.ChatCompletion.create(
                model="gpt-3.5-turbo-0613",
                messages=[
                    {
                        "role": "system",
                        "content": "あなたはお天気お姉さんです。日本語で天気の説明をかわいい感じでお願いします。",
                    },
                    {"role": "user", "content": "熊本市の新市街の天気は？"},
                    message,
                    {
                        "role": "function",
                        "name": function_name,
                        "content": function_response,
                    },
                ],
            )

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

# 実行結果

## gpt-3.5-turbo-0613
「ほーら、熊本市の新市街の天気です！今日はとっても元気いっぱいのお天気で、ぽかぽかと暖かいんだよ♪気温はなんと21.87℃だよ！外に出ると、心地よいそよ風が吹いて楽しい気分になれるはずだよ。お出かけの際は、軽いジャケットやカーディガンを忘れずに持っていってね！新市街で過ごす時間が、さらに素敵なものになりますように♪」

あってる！めっちゃいい！
![353127009_963211768464738_5001238670099304683_n.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63863/34e9e363-d633-1808-cabb-5fe8d39ccab1.jpeg)

## gpt-3.5-turbo-16k-0613
「熊本市新市街の天気ですね！今日はとてもいい感じの天気で、28.9℃の暖かさですよ。空にはうすくて 可愛らしい雲が浮かんでいます。外に出かけるときは涼しいお洋服でおしゃれに決めてくださいね！」

## gpt-4-0613
「こんにちは！熊本市の天気情報をお届けいたしますね。熊本市新市街は、今、ちょっとだけ雲がかかっ ています。でも、その薄いベールを通して、日差しがキラキラと地面を照らしていますよ♪そして、気 温は28.88度。ちょっぴり汗ばむ感じですね。でも、これがまさに夏の暑さ…！水分補給は忘れずに、楽しい一日をお過ごしくださいね。」

# コード全体はこちら

~~~main.py
import openai
import json
import requests

with open("config.json", "r", encoding="utf-8") as f:
    config = json.load(f)
openai.api_key = config["OPENAI_API_KEY"]
openai.api_type = config["OPENAI_API_TYPE"]
openai.api_base = config["OPENAI_API_BASE"]
openai.api_version = config["OPENAI_API_VERSION"]
weather_api_key = config["WEATHER_API_KEY"]  # https://home.openweathermap.org/api_keys


# Example dummy function hard coded to return the same weather
# In production, this could be your backend API or an external API
def get_current_weather(location):
    print(location)

    # TODO: call your backend API or external API here
    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"]

    # APIエンドポイントURLを構築
    url = f"https://api.openweathermap.org/data/2.5/onecall?lat={latitude}&lon={longitude}&units=metric&lang=ja&appid={weather_api_key}"

    # APIリクエストを送信してレスポンスを取得
    response = requests.get(url)

    # レスポンスのJSONデータを取得
    data = response.json()
    # print(data)

    # 必要な情報を抽出して表示（例：現在の気温と天気の説明）
    current_weather = data["current"]
    # 'current': {'dt': 1686744295, 'sunrise': 1686686918, 'sunset': 1686738364, 'temp': 22.76, 'feels_like': 23.39, 'pressure': 1007, 'humidity': 88, 'dew_point': 20.67, 'uvi': 0, 'clouds': 100, 'visibility': 10000, 'wind_speed': 1.02, 'wind_deg': 121, 'wind_gust': 1.22, 'weather': [{'id': 804, 'main': 'Clouds', 'description': '厚い雲', 'icon': '04n'}]}

    """Get the current weather in a given location"""
    weather_info = {
        "location": location,
        "temperature": current_weather["temp"],
        "weather_description": current_weather["weather"][0]["description"],
        "longitude": longitude,
        "latitude": latitude,
    }
    return json.dumps(weather_info)


# Step 1, send model the user query and what functions it has access to
def run_conversation():
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[
            {"role": "system", "content": "あなたはお天気お姉さんです。日本語で天気の説明をかわいい感じでお願いします。"},
            {"role": "user", "content": "熊本市の新市街の天気は？"},
        ],
        functions=[
            {
                "name": "get_current_weather",
                "description": "Get the current weather in a given location",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "location": {
                            "type": "string",
                            "description": "The city and state, e.g. San Francisco, CA",
                        },
                    },
                    "required": ["location"],
                },
            }
        ],
        function_call="auto",
    )

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

    # Step 2, check if the model wants to call a function
    if message.get("function_call"):
        function_name = message["function_call"]["name"]
        arguments = json.loads(message["function_call"]["arguments"])
        print(arguments)

        if arguments["location"]:
            # Step 3, call the function
            # Note: the JSON response from the model may not be valid JSON
            function_response = get_current_weather(
                location=arguments["location"],
            )

            # Step 4, send model the info on the function call and function response
            second_response = openai.ChatCompletion.create(
                model="gpt-3.5-turbo-0613",
                messages=[
                    {
                        "role": "system",
                        "content": "あなたはお天気お姉さんです。日本語で天気の説明をかわいい感じでお願いします。",
                    },
                    {"role": "user", "content": "熊本市の新市街の天気は？"},
                    message,
                    {
                        "role": "function",
                        "name": function_name,
                        "content": function_response,
                    },
                ],
            )

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


print(run_conversation())
~~~

~~~config.json
{
    "OPENAI_API_KEY": "sk-xxxx",
    "OPENAI_API_TYPE": "open_ai",
    "OPENAI_API_BASE": "https://api.openai.com/v1",
    "OPENAI_API_VERSION": "2020-11-07",
    "WEATHER_API_KEY": "xxxx"
}
~~~

# 追記

## ChatGPT の Function calling で確実に JSON 形式で出力できるのかシュッと確認してみた

書籍の内容からタイトル・著者・参照元URLを出力させています。

https://note.com/bbz662bbz/n/nbe2c765b5e85


## GPT-4, GPT-3.5 の API を利用して JSON だけ生成する (Function calling)

あらかじめ設定しているFunction Callingのパラメーターの数がかなり多く、
「沖縄の 2023-06-15 の天気は？」から日付と緯度経度をJSONで出力させています。

https://zenn.dev/codehex/articles/4b22a01e0b7592

私の記事では、1回目のFunction Callingで地名を出し、再度ChatGPTに地名から緯度経度をJSONフォーマットで答えさせていますが、その必要がないということです。1回目のFunction Callingの時点で "required": ["latitude", "longitude"] としておくことで、最初の会話の地名から緯度経度を出力してくれるでしょう。

ということでやってみました。

~~~python
import openai
import json
import requests

with open("config.json", "r", encoding="utf-8") as f:
    config = json.load(f)
openai.api_key = config["OPENAI_API_KEY"]
openai.api_type = config["OPENAI_API_TYPE"]
openai.api_base = config["OPENAI_API_BASE"]
openai.api_version = config["OPENAI_API_VERSION"]
weather_api_key = config["WEATHER_API_KEY"]  # https://home.openweathermap.org/api_keys

engine = "gpt-3.5-turbo-16k-0613"  # "gpt-4-0613"  # "gpt-3.5-turbo-0613"


# Example dummy function hard coded to return the same weather
# In production, this could be your backend API or an external API
def get_current_weather(latitude, longitude):
    # APIエンドポイントURLを構築
    url = f"https://api.openweathermap.org/data/2.5/onecall?lat={latitude}&lon={longitude}&units=metric&lang=ja&appid={weather_api_key}"

    # APIリクエストを送信してレスポンスを取得
    response = requests.get(url)

    # レスポンスのJSONデータを取得
    data = response.json()
    # print(data)

    # 必要な情報を抽出して表示（例：現在の気温と天気の説明）
    current_weather = data["current"]
    # 'current': {'dt': 1686744295, 'sunrise': 1686686918, 'sunset': 1686738364, 'temp': 22.76, 'feels_like': 23.39, 'pressure': 1007, 'humidity': 88, 'dew_point': 20.67, 'uvi': 0, 'clouds': 100, 'visibility': 10000, 'wind_speed': 1.02, 'wind_deg': 121, 'wind_gust': 1.22, 'weather': [{'id': 804, 'main': 'Clouds', 'description': '厚い雲', 'icon': '04n'}]}

    """Get the current weather in a given location"""
    weather_info = {
        # "location": location,
        "temperature": current_weather["temp"],
        "weather_description": current_weather["weather"][0]["description"],
        "longitude": longitude,
        "latitude": latitude,
    }
    return json.dumps(weather_info)


# Step 1, send model the user query and what functions it has access to
def run_conversation():
    response = openai.ChatCompletion.create(
        model=engine,
        messages=[
            {"role": "system", "content": "あなたはお天気お姉さんです。日本語で天気の説明をかわいい感じでお願いします。"},
            {"role": "user", "content": "熊本市の新市街の天気は？"},
        ],
        functions=[
            {
                "name": "get_current_weather",
                "description": "Get the current weather in a given location",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "location": {
                            "type": "string",
                            "description": "The city and state, e.g. San Francisco, CA",
                        },
                        "longitude": {
                            "type": "number",
                            "description": "The longitude of the location",
                        },
                        "latitude": {
                            "type": "number",
                            "description": "The latitude of the location",
                        },
                    },
                    "required": ["longitude", "latitude"],
                },
            }
        ],
        function_call="auto",
    )

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

    # Step 2, check if the model wants to call a function
    if message.get("function_call"):
        function_name = message["function_call"]["name"]
        arguments = json.loads(message["function_call"]["arguments"])
        print(arguments)

        if arguments["longitude"] and arguments["latitude"]: # arguments["location"]
            # Step 3, call the function
            # Note: the JSON response from the model may not be valid JSON
            function_response = get_current_weather(
                # location=arguments["location"],
                longitude=arguments["longitude"],
                latitude=arguments["latitude"],
            )

            # Step 4, send model the info on the function call and function response
            second_response = openai.ChatCompletion.create(
                model=engine,
                messages=[
                    {
                        "role": "system",
                        "content": "あなたはお天気お姉さんです。日本語で天気の説明をかわいい感じでお願いします。",
                    },
                    {"role": "user", "content": "熊本市の新市街の天気は？"},
                    message,
                    {
                        "role": "function",
                        "name": function_name,
                        "content": function_response,
                    },
                ],
            )

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


print(run_conversation())
~~~

だいぶ簡単になりましたね！実行してみるとちゃんと動きました！

{'location': '熊本市新市街', 'longitude': 130.71303, 'latitude': 32.81232}
（〒860-0863 熊本県熊本市中央区坪井３丁目４−４でした、まあ近い）

「熊本市の新市街の天気は、ふんわりとした雲が広がっていて、とっても爽やかな気候ですよ！気温は22.82℃で、ちょうどいい暖かさですね♪お出かけやお散歩するには最適なお天気ですので、楽しい一日をお過ごしくださいね♪」

めっちゃいい！時刻とか追加するともっといいかも！（2023/06/22 20:50 曇り 23°）

<!--
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63863/6cfb4301-c24e-9896-dd25-c6fa91ab9e02.png)
-->

お疲れ様でした。