OpenAI APIのFunction calling機能でGPTに検索結果に基づいた回答をさせてみる

2023.06.19

こんちには。

データアナリティクス事業本部 インテグレーション部 機械学習チームの中村です。

本記事では、Function callingの応用として、SerpApiとBeautiful Soupを使い、GPT-3.5に検索結果に基づいた回答をさせて見たいと思います。

Function callingとは

Function callingは、自身で定義した処理を組み込んだチャットが実現できる機能となっています。

与えられたfunctionの引数や説明をOpenAI APIのチャットが確認して使用する関数、引数を決める形で実行されます。

そのため、OpenAI APIへのChatCompletionは複数回呼ばれるような仕組みです。

いくつか弊社のブログでも記事が出ておりますので、そちらもご参照ください。

作ってみる

それでは実際にSerpApiとBeautiful Soupを使って作っていきたいともいます。

各種APIキーの準備

ここは他の記事でも言及がありますので、その記事の紹介にとどめます。

OpenAI APIキーについては以下をご参照ください。

SerpApiのAPIキーについては以下を参照ください。(無償版は検索回数が限られているのでご注意ください)

実行環境

Google Colaboratoryを使います。

!python --version
Python 3.10.12

必要なライブラリを入れておきます。

!pip install openai google-search-results

ライブラリのバージョンは以下となりました。

!pip freeze | grep -e "openai" -e "google-search-results"
google-search-results==2.4.2
openai==0.27.8

APIキーの設定

以下のように設定して置きます。

OPENAI_API_KEY="{skで始まるOPENAI APIキー}"
SERPAPI_API_KEY="{SerpApiのAPIキー}"

OpenAI APIの単体サンプルコード

いつも通りの記述ですが、以下のようにChatCompletionを呼び出します。

import openai

openai.api_key = OPENAI_API_KEY

model_name = "gpt-3.5-turbo-16k-0613"

question = "PythonでOpenAI APIを使う方法"

response = openai.ChatCompletion.create(
    model=model_name,
    messages=[
        {"role": "user", "content": question},
    ],
)
print(response.choices[0]["message"]["content"].strip())

SerpAPIの単体サンプルコード

以下のように、site:dev.classmethod.jpを付けたクエリとすれば、クラスメソッドのブログ内に限定して検索ができます。

from serpapi import GoogleSearch

query_str = "PythonでOpenAI APIを使う方法"

search = GoogleSearch({
    "q": f"site:dev.classmethod.jp {query_str}",
    "api_key": SERPAPI_API_KEY
    })

result = search.get_dict()

検索結果のURLのリストは、organic_resultslinkを集めてくれば取得できます。

address_list = [result['link'] for result in result['organic_results']]
address_list
['https://dev.classmethod.jp/articles/openai-api-chat-python-first-step/',
 'https://dev.classmethod.jp/articles/first_step-openai_api/',
 'https://dev.classmethod.jp/articles/openai-api-quickstart-tutorial/',
 'https://dev.classmethod.jp/articles/openai-create-images/',
 'https://dev.classmethod.jp/articles/azure-openai-from-python-module/',
 'https://dev.classmethod.jp/articles/understand-openai-function-calling/',
 'https://dev.classmethod.jp/articles/888c355f2c88e117d172ec1bd3d28a435ee438766630638e3e9f7887aef8f5ee/',
 'https://dev.classmethod.jp/articles/search-with-openai-embeddings/',
 'https://dev.classmethod.jp/articles/open-api-lambda-test/',
 'https://dev.classmethod.jp/articles/tried-the-code-in-the-openai-cookbook/']

SerpApiの使い方の詳しい説明は以下も参照下さい。

Beautiful Soupの単体サンプルコード

urllibでHTMLを取得して、Beautiful Soupでパースします。

import urllib.request
from bs4 import BeautifulSoup

# HTMLを取得
req = urllib.request.Request(address_list[0])
with urllib.request.urlopen(req) as res:
    body = res.read()
html_doc = body.decode()

# パース処理
soup = BeautifulSoup(html_doc, 'html.parser')
contents = soup.find('div', class_="content")
texts = [c.get_text() for c in contents.find_all('p')]
texts = "\n\n".join(texts)
print(texts[:4000])

HTMLのパース処理は、クラスメソッドのブログのHTML構成に合わせています。

別のソースを使用する場合、この部分はそのまま使用できないので注意が必要です。

ここはLlamaIndexなどで実装されているParserを使うのも手だと思いますが、今回は自作しています。

またトークン数が多くなることを防ぐために、文字数も4000文字に制限しています。

Function callingに使用するユーザ関数の定義

それぞれの単体サンプルコードを元にして、Function callingに使用するユーザ関数の定義していきます。

まずはSerpApiを使って検索するユーザ関数を以下のように定義します。

from serpapi import GoogleSearch

def search_cm_blog(query: str) -> str:

    search = GoogleSearch({
    "q": f"site:dev.classmethod.jp {query_str}",
    "api_key": SERPAPI_API_KEY
    })

    result = search.get_dict()

    address_list = [result['link'] for result in result['organic_results']]
    address_list
    return str(address_list)

検索型のポイントは、得られるURLのリストを文字列化している点です。

これはFunction callingの結果をChatCompletionのmessage履歴に含める際に、そのままlist型のデータが使用できないため、このような実装となっています。

次に、URLのデータを取得するユーザ関数を以下のように定義します。

import urllib.request
from bs4 import BeautifulSoup

def get_blog_contents(url: str) -> str:
    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as res:
        body = res.read()
    html_doc = body.decode()

    soup = BeautifulSoup(html_doc, 'html.parser')
    contents = soup.find('div', class_="content")
    texts = [c.get_text() for c in contents.find_all('p')]
    texts = "\n\n".join(texts)

    return texts[:4000]

こちらについては、ほぼサンプルコードのままとなっています。

Function callingの設定

OpenAI APIに渡すfunctionsを以下のように定義して、Function callingを使用できるようにします。

functions=[
    {
        "name": "search_cm_blog",
        "description": "指定したキーワードでクラスメソッドのブログを検索して、URLのリストを得る。",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "検索キーワード",
                },
            },
            "required": ["query"],
        },
    },
    {
        "name": "get_blog_contents",
        "description": "指定したURLについてその内容を取得して、パースした結果のテキストを得る。",
        "parameters": {
            "type": "object",
            "properties": {
                "url": {
                    "type": "string",
                    "description": "内容を取得したいページのURL",
                },
            },
            "required": ["url"],
        },
    }
]

なるべく詳しく書くように努力しています。

Function callingを使用したOpenAI API呼び出し

最後に、Function callingを使用したOpenAI API呼び出しを以下のように記述しました。

少し長いですが、後程補足します。

import openai

openai.api_key = OPENAI_API_KEY

model_name = "gpt-3.5-turbo-16k-0613"

query_str = "PythonでOpenAI APIを使う方法"

question = f"""
「{query_str}」について、まずクラスメソッドのブログを検索した結果のその上位3件を取得します。
その後、それぞれのURLについてその内容を取得して、パースした結果のテキスト得ます。
そしてそれらのパースした結果をまとめ、最終的な答えを1つ生成してください。
"""

MAX_REQUEST_COUNT = 10

message_history = []

for request_count in range(MAX_REQUEST_COUNT):

    function_call_mode = "auto"
    if request_count == MAX_REQUEST_COUNT - 1:
        function_call_mode = "none"

    response = openai.ChatCompletion.create(
        model=model_name,
        messages=[
            {"role": "user", "content": question},
            *message_history,
        ],
        functions=functions,
        function_call=function_call_mode,
    )

    if response["choices"][0]["message"].get("function_call"):

        print("*** Function calling ***")
        print(response["choices"][0]["message"].get("function_call"))

        message = response["choices"][0]["message"]
        message_history.append(message)

        function_call = response["choices"][0]["message"].get("function_call")
        function_name = function_call.get("name")

        if function_name in [f["name"] for f in functions]:
            function_arguments = function_call.get("arguments")
            function_response = eval(function_name)(**eval(function_arguments))
        else:
            raise Exception

        message = {
            "role": "function",
            "name": function_name,
            "content": function_response,
        }

        message_history.append(message)

    else:

        print("*** Final answer ***")
        print(response.choices[0]["message"]["content"].strip())
        break

実行結果は以下のようになります。

*** Function calling ***
{
  "name": "search_cm_blog",
  "arguments": "{\n  \"query\": \"Python OpenAI API\"\n}"
}
*** Function calling ***
{
  "name": "get_blog_contents",
  "arguments": "{\n  \"url\": \"https://dev.classmethod.jp/articles/openai-api-chat-python-first-step/\"\n}"
}
*** Function calling ***
{
  "name": "get_blog_contents",
  "arguments": "{\n  \"url\": \"https://dev.classmethod.jp/articles/first_step-openai_api/\"\n}"
}
*** Function calling ***
{
  "name": "get_blog_contents",
  "arguments": "{\n  \"url\": \"https://dev.classmethod.jp/articles/openai-api-quickstart-tutorial/\"\n}"
}
*** Final answer ***
以下はクラスメソッドのブログ記事についてのまとめです:

1. [OpenAI APIを使ってPythonでChatGPTを使う超基本的な使い方](https://dev.classmethod.jp/articles/openai-api-chat-python-first-step/)

この記事では、PythonでChatGPTを使うための超基本的な使い方が紹介されています。APIキーの発行方法から始まり、APIのインストールや環境変数の設定、サンプルコードの解説まで詳しく説明されています。

2. [OpenAI APIを使ってChatGPTを導入する方法](https://dev.classmethod.jp/articles/first_step-openai_api/)

この記事では、OpenAI APIを使ってChatGPTを導入する方法が解説されています。APIキーの取得方法やデモ用コードの使い方、実際にChatGPTを使って動物の名前を生成するアプリケーションの作り方などが説明されています。

3. [OpenAI API Quickstart Tutorialをなぞってみた](https://dev.classmethod.jp/articles/openai-api-quickstart-tutorial/)

この記事では、OpenAI APIのQuickstartチュートリアルをなぞった結果が紹介されています。チュートリアルの進め方やAPIキーの取得方法、プロンプトの指定方法などが詳しく解説されています。

以上の記事を参考にすると、PythonでOpenAI APIを使ってChatGPTを導入し、基本的な使い方を理解することができます。

このように、最初にsearch_cm_blogを呼び出し、その後get_blog_contentsを3回呼び出し、最終的な結果を得る想定通りの動作をしてくれることが分かりました。

複雑なFunction callingの使い方が良く分かった気がします。

実装のポイント1 : evalを使って定義した関数を呼ぶ

使用したい関数は、OpenAI APIに決めてもらいますが、その結果は全て文字列となっていますので、eval()をうまく使って関数を呼び出します。

        function_call = response["choices"][0]["message"].get("function_call")
        function_name = function_call.get("name")

        if function_name in [f["name"] for f in functions]:
            function_arguments = function_call.get("arguments")
            function_response = eval(function_name)(**eval(function_arguments))
        else:
            raise Exception

まずeval(function_name)を使って関数を実体化させます。

次に引数も文字列となっているのでeval(function_arguments)で実体化し、また引数の種類は関数によって異なることから、得られたdict型を**で展開(アンパック)して関数に与えています。

なお、念のためfunctionsにない関数が呼ばれないように例外処理を設けています。

実装のポイント2 : 最大のリクエスト回数を決めておく

function_callをautoとすると、ずっと処理が終わらない可能性があるので、最大のリクエスト回数を決めています。

最大のリクエスト回数を超過しそうになると、function_callをnoneに設定してそこまでで処理を終わらせます。

for request_count in range(MAX_REQUEST_COUNT):

    function_call_mode = "auto"
    if request_count == MAX_REQUEST_COUNT - 1:
        function_call_mode = "none"

実装のポイント3 : 途中のレスポンスとfunctionの実行結果は履歴として与える

それまでのやり取りをmessage_historyに格納しておくことで、続きの結果を得ることができます。

ですので、そこまでの途中のレスポンスに加えて、functionの実行結果も履歴として格納しておきます。

    if response["choices"][0]["message"].get("function_call"):

        print("*** Function calling ***")
        print(response["choices"][0]["message"].get("function_call"))

        message = response["choices"][0]["message"]
        message_history.append(message)

        message = {
            "role": "function",
            "name": function_name,
            "content": function_response,
        }

        message_history.append(message)

functionの実行結果は、"role": "function"という形でOpenAI APIでは見分けているようですね。

まとめ

いかがでしたでしょうか。

OpenAI APIのFunction callingの応用例についてご紹介致しました!

複数のfunctionがある場合でもうまく動作させることができました。本記事がFunction callingを使用される方の参考になれば幸いです。