ChatgptのFunction Callingを使って、最新作のポケモンの情報を取得してみる

# はじめに
Function callingは誤解を恐れずに簡単に説明すると、gptが関数の呼び出し、引数を考えて選択し回答をしてくれる機能です。

> 利用用途
> - 外部ツール (ChatGPT プラグインなど) を呼び出して質問に答えるチャットボットを作成する
> - 自然言語を API 呼び出しまたはデータベース クエリに変換する
> - テキストから構造化データを抽出する

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

# 作成したもの
今回はポケモンの最新作スカーレット、バイオレットに初登場したポケモンであるニャオハのデータをFuncion Callingを使用して取得できるかを検証します。

まずは普通にChatGPT4に聞いてみた結果

![スクリーンショット 2023-06-25 23.16.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/706631/a071b989-9c6c-92b6-6a05-67a92ee71faa.png)

普通に質問してもこのようにGPT4でもニャオハのデータはありません。

そこでPokeAPIを使用してニャオハの情報を取得し、その情報をもとに回答してくれる機能を作成します。

## PokeAPI
PokeAPIはポケモンの情報を取得できるAPIです。
ラッパーライブラリなどが色々提供されていたりしますが、今回は単純にpokemonの情報を取得できる

```
url = f"https://pokeapi.co/api/v2/pokemon/{name}/"
```

こちらのurlを使用します。
{name}の中身はポケモンのIDか英語名のみ使用できます。

そのため、今回入力値はニャオハの英語名であるsprigatitoで試しました。
ここは何か変換するロジックなどを考えたいところです。

https://pokeapi.co/docs/v2

## Function Callingの実装と仕組み
今回は２つの関数を実装しました。

### get_pokemon_description_info
Function Callingで呼び出す関数。
この関数が呼び出されるかどうかをGPTが判断してくれる

- inputはポケモンの英語名
- outputはポケモンのid,名前、重さ、高さ、覚える技に設定

### get_pokemon_description
Function Callingの呼び出し元
単純にChatCompletionしている部分です。

ここで、Function Callingによってget_pokemon_description_infoから情報が返された場合、その情報をもとにレスポンスを返すというコードを作成しています。

## 実装の中身

### 準備
```python
import os
import sys
import openai
import json
import requests
from dotenv import load_dotenv
import pandas as pd

load_dotenv()

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

```

## Function Callingで呼び出す関数の定義(get_pokemon_description_info)

```python
def get_pokemon_description_info(pokemon_name):
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[
            {
                "role": "system",
                "content": 'JSONフォーマットで答えてください。そして、レスポンスはすべて小文字にしてください。{"name": { string }}',
            },
            {
                "role": "user",
                "content": f"ポケモンである{pokemon_name}の正式な英語名は？(そして、レスポンスはすべて小文字にしてください。) もし、わからない場合やすでに英語が入力された場合は入力をそのままjson形式で出力してください",
            },
        ],
    )

    message = response["choices"][0]["message"]
    json_data = json.loads(message["content"])
    print(f"json_data:{json_data}")
    name = json_data['name']
    print(f"name:{name}")

    url = f"https://pokeapi.co/api/v2/pokemon/{name}/"

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

    # print(f"json_data:{json_data}")

    pokemon_info = {
        "id": json_data['id'],
        "name": json_data['name'],
        "weight": json_data['weight'] / 10,
        "height": json_data['height'] / 10,
        "moves": json_data['moves'][:3],
    }

    print(f"pokemon_info:{pokemon_info}")
    return json.dumps(pokemon_info)
```

プロンプトを最初英語名も出してくれるようになるか試したんですが、ニャオハの情報がそもそもないので、英語名も知っているはずがなく、変な記載をしています。
ここはロジックを描く必要がありそうです。

この関数はpokeAPIからニャオハのid,name,weight,height,moves(覚える技)の情報を取ってきて、jsonデータとして格納する関数です。

### 上記で定義した関数を呼び出す部分(get_pokemon_description)

```python
def get_pokemon_description():
    if len(sys.argv) > 1:
        pokemon_name = sys.argv[1]
    else:
        pokemon_name = input("ポケモン名を入力してください: ")
    
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[
            {
                "role": "system",
                "content": "日本語で正確にポケモンのIDと名前、重さと高さと覚える技を紹介してください"
            },
            {
                "role": "user",
                "content": f"{pokemon_name}の名前とID、重さと高さと覚える技は？"
            },
        ],
        functions=[{
            "name": "get_pokemon_description_info",
            "description": "ポケモンの情報を取得する",
            "parameters": {
                "type": "object",
                "properties": {
                    "pokemon_name": {
                        "type": "string",
                        "description": "ポケモンの名前"
                    },
                },
                "required": ["pokemon_name"]
            },
        }],
        function_call="auto",
    )

    message = response["choices"][0]["message"]
    print(f"get_pokemon_description_message:{message}")
    
    if message.get("function_call"):
        function_name = message["function_call"]["name"]
        arguments = json.loads(message["function_call"]["arguments"])
        print(f"function: {function_name}, arguments: {arguments}")

        if arguments["pokemon_name"]:
            function_response = get_pokemon_description_info(arguments["pokemon_name"])
            print(f"function_response: {function_response}")

            second_response = openai.ChatCompletion.create(
                model="gpt-3.5-turbo-0613",
                messages=[
                    {
                        "role": "system",
                        "content": "日本語で正確にポケモンのIDと名前と重さと高さと覚える技を教えてください。重さは"
                    },
                    {
                        "role": "user",
                        "content": f"{pokemon_name}の名前とIDと重さと高さと覚える技は？"
                    },
                    message,
                    {
                        "role": "function",
                        "name": function_name,
                        "content": str(function_response),
                    }
                ]
            )

            message = second_response["choices"][0]["message"]
            # print(f"second_response_message: {message}")
            print(message["content"])
            return second_response
```

ここではFunction Callingの設定、Function Callingで関数を呼び出した場合の処理をif文で記述しています。

(コードがところどころ変な書き方になっているので、もっとちゃんと仕組みを理解していく必要があります。)

```python
        functions=[{
            "name": "get_pokemon_description_info",
            "description": "ポケモンの情報を取得する",
            "parameters": {
                "type": "object",
                "properties": {
                    "pokemon_name": {
                        "type": "string",
                        "description": "ポケモンの名前"
                    },
                },
                "required": ["pokemon_name"]
            },
        }],
        function_call="auto",
```

この部分でfunction callingの設定をしていて、関数の説明や引数を設定することで、関数を呼ぶかどうか、引数に何をいれるべきかgptが判断しやすくなります。

function_call="auto"で関数を呼ぶか呼ばないかを自動で判断してくれるようにしてますが、ここは明示することも可能です。

### コード全文
```python
# open ai apiのfunction callingでPokeAPIを呼び出す

import os
import sys
import openai
import json
import requests
from dotenv import load_dotenv
import pandas as pd

load_dotenv()

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

def get_pokemon_description_info(pokemon_name):
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[
            {
                "role": "system",
                "content": 'JSONフォーマットで答えてください。そして、レスポンスはすべて小文字にしてください。{"name": { string }}',
            },
            {
                "role": "user",
                "content": f"ポケモンである{pokemon_name}の正式な英語名は？(そして、レスポンスはすべて小文字にしてください。) もし、わからない場合やすでに英語が入力された場合は入力をそのままjson形式で出力してください",
            },
        ],
    )

    message = response["choices"][0]["message"]
    json_data = json.loads(message["content"])
    print(f"json_data:{json_data}")
    name = json_data['name']
    print(f"name:{name}")

    url = f"https://pokeapi.co/api/v2/pokemon/{name}/"

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

    # print(f"json_data:{json_data}")

    pokemon_info = {
        "id": json_data['id'],
        "name": json_data['name'],
        "weight": json_data['weight'] / 10,
        "height": json_data['height'] / 10,
        "moves": json_data['moves'][:3],
    }

    print(f"pokemon_info:{pokemon_info}")
    return json.dumps(pokemon_info)

def get_pokemon_description():
    if len(sys.argv) > 1:
        pokemon_name = sys.argv[1]
    else:
        pokemon_name = input("ポケモン名を入力してください: ")
    
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[
            {
                "role": "system",
                "content": "日本語で正確にポケモンのIDと名前、重さと高さと覚える技を紹介してください"
            },
            {
                "role": "user",
                "content": f"{pokemon_name}の名前とID、重さと高さと覚える技は？"
            },
        ],
        functions=[{
            "name": "get_pokemon_description_info",
            "description": "ポケモンの情報を取得する",
            "parameters": {
                "type": "object",
                "properties": {
                    "pokemon_name": {
                        "type": "string",
                        "description": "ポケモンの名前"
                    },
                },
                "required": ["pokemon_name"]
            },
        }],
        function_call="auto",
    )

    message = response["choices"][0]["message"]
    print(f"get_pokemon_description_message:{message}")
    
    if message.get("function_call"):
        function_name = message["function_call"]["name"]
        arguments = json.loads(message["function_call"]["arguments"])
        print(f"function: {function_name}, arguments: {arguments}")

        if arguments["pokemon_name"]:
            function_response = get_pokemon_description_info(arguments["pokemon_name"])
            print(f"function_response: {function_response}")

            second_response = openai.ChatCompletion.create(
                model="gpt-3.5-turbo-0613",
                messages=[
                    {
                        "role": "system",
                        "content": "日本語で正確にポケモンのIDと名前と重さと高さと覚える技を教えてください。重さは"
                    },
                    {
                        "role": "user",
                        "content": f"{pokemon_name}の名前とIDと重さと高さと覚える技は？"
                    },
                    message,
                    {
                        "role": "function",
                        "name": function_name,
                        "content": str(function_response),
                    }
                ]
            )

            message = second_response["choices"][0]["message"]
            print(f"second_response_message: {message}")
            print(message["content"])
            return second_response

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

## 実行結果

```
$ pipenv run python test.py sprigatito

sprigatitoの情報は以下の通りです：
- ID: 906
- 名前: sprigatito
- 重さ: 4.1kg
- 高さ: 0.4m
- 覚える技: 
  - レベルアップで覚える技:
    - scratch (ひっかく) - レベル1で覚える
    - tail-whip (しっぽをふる) - レベル1で覚える
  - 技マシンで覚える技:
    - take-down (とっしん) - 技マシンで覚える
```

このようにニャオハの情報を応答することができました！

# あとがき

これを利用してなにか便利なwebアプリを作りたいなと思うのですが、いいアイデアが思いつきません
もし可能であればアイデアをください！

記事を書いた人のtwitterなので、よかったらフォローなどお願いします!
https://twitter.com/js_manabitai

# 注意事項
私は最初Function Callingを使用すればgptが外部APIを実行してくれるのかと思っていました。
しかし、GPTは外部APIを実行することはできないため、そこは注意が必要です。

# 参考記事

https://qiita.com/hedgehog051/items/e9724af2786c5e262e62

https://qiita.com/Syoitu/items/d4833cc0a0663fdfeed3