【LLM】OpenAI FunctionCallingでお天気チャットボット

# 記事コンテンツ
OpenAIの`FunctionCalling`を利用して**LLMが今日の天気を回答できる機能**を実装してみました。みなさんもご存じのとおり、今日の天気を聞いてもChatGPTはわからないと回答します。なぜならChatGPTには今日の天気を知る術がないからです。OpenAIの`FunctionCalling`を利用することで、例えば、あらかじめ作っておいたお天気情報取得関数をLLMとうまく組み合わせることができ、お天気チャットボットの実現が可能になります。`FunctionCalling`そのものについては[こちら](https://qiita.com/sakue_103/items/d733ee832d880f2686b9)が参考になるかと思います。

# 仕組み
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/377231/6630ab5c-b032-451b-97cc-0bf6d073f22e.png)

# コード
```python
import os
import json
import openai
from dotenv import load_dotenv
import requests

# OPENAI_API_kEY
os.environ['OPENAI_API_KEY'] = 'sk-************************************'
load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")

# function callingを実行可能な関数
def function_calling(functions_metadata:list, messages:list, user_input:str) -> dict:
    if user_input is not None:
        messages.append({"role": "user", "content": user_input})
    resposne = openai.chat.completions.create(
        model = "gpt-4o",
        messages = messages,
        functions=functions_metadata,
        function_call="auto"
    )
    return resposne

# 天気情報を取得するための関数
def get_weather(query:str) -> str:
    url = "https://www.jma.go.jp/bosai/forecast/data/forecast/090000.json" # 栃木県宇都宮市の天気場情報
    response = requests.get(url)
    data = response.json()
    weather = data[0]["timeSeries"][0]["areas"][0]["weathers"][0].replace("　","")
    max_temp = data[1]["tempAverage"]["areas"][0]["max"]
    min_temp = data[1]["tempAverage"]["areas"][0]["min"]
    result = {
        "query": query,
        "result": "今日の栃木県の天気は"+weather+"です。また最高気温は"+max_temp+"℃、最低気温は"+min_temp+"℃です。",
    }
    
    return json.dumps(result)

# 関数のメタデータ
get_weather_metadata = {
    "name": "get_weather",
    "description": "APIを用いて栃木県の天気情報を取得するための関数。外部情報を取得可能。",
    "parameters": {
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "天気情報についてのクエリ"
            },
        },
        "required": ["query"]
    }
}

# 関数のメタデータをリストに格納
functions_metadata = [
    get_weather_metadata
]

# 関数名をキーにして、関数を呼び出せるようにしておく
functions_callable = {
    get_weather_metadata["name"]: get_weather
}

# システムのプロンプト
SYSTEM_PROMPT = """
あなたはユーザを助けるアシスタントです。
ユーザの入力に正しく回答を出力するために、ステップバイステップで慎重に考えることができます。
"""

# 文脈を保持するリスト
messages = [
    {"role":"system", "content": SYSTEM_PROMPT},
]

# ユーザのクエリ
user_input = """
必要に応じてfunction_callを使用する。
今日の栃木県の天気は何ですか？
"""

# 推論実行
res = function_calling(functions_metadata, messages, user_input)

# function_callがある場合は関数を呼び出す
if res.choices[0].message.function_call:
    # functionを実行するために必要な情報を取得する
    func_name = res.choices[0].message.function_call.name
    func_parameters = json.loads(res.choices[0].message.function_call.arguments)

    # functionを実行する
    func_result = functions_callable[func_name](**func_parameters)
    
    # 実行結果をmessageに追加する
    messages.append(
        {
            "role": "function", 
            "name": func_name, 
            "content": func_result
        }
    )

    # functionの実行結果を踏まえて再度推論実行する
    res = function_calling(functions_metadata, messages, None)

print(res.choices[0].message.content)
```

今回、関数の中でお天気情報取得APIのURLをユーザのクエリに合わせて動的に変化させるのが面倒だったことに加え、メインは`FunctionCalling`が機能するかどうかの検証だったので場所は栃木県宇都宮市に限定しました。そのため関数の引数はなしとしました。

ユーザの入力クエリはこちらです。
```python
user_input = """
必要に応じてfunction_callを使用する。
今日の栃木県の天気は何ですか？
"""
```

```:実行結果
今日の栃木県の天気は晴れのちくもりです。また、最高気温は10.1℃、最低気温は-1.0℃です。
```
期待通りの結果となりました。便利ですね。リアルタイムの情報を取得しLLMに連携できるのは魅力的です。この場合はこうさせたいってことが具体的に決まっていれば特に`FunctionCalling`は有力な方法だと感じます。