リモート開発メインのソフトウェア開発企業のエンジニアブログです

PythonでChatGPT APIを使ってみる 第2回 Bing Search APIとの連携

概要

前回の記事ではChatGPT APIの基本的な使い方を解説しました。

今回はChatGPT APIとBing Search APIとの連携方法について、記事にしようと思います。

なぜBing Search APIと連携させたいか

そもそもChatGPTのGPT-4は、2023年4月のデータまでしか学習していません。

ブラウザ上でChatGPTを使用する場合はWeb上を検索して最新の情報を取得してくれることはありますが、APIからChatGPTを利用する場合は最新の情報を正確には答えてくれません。

例えば前回の記事で作成したプログラムに「2024年1月にDAMカラオケで一番歌われた曲はなんで すか?」という質問をした所、以下の答えが返ってきました。

あなたの質問を入力してください (終了するには 'quit' と入力): 2024年1月にDAMカラオケで一番歌われた曲はなんで すか?
Assistant: 申し訳ありませんが、私の最後のアップデートは2023年であり、それ以降のデータやイベントについては情 報がありません。2024年1月にDAMカラオケで最も歌われた曲に関する最新情報は、DAMの公式サイトや関連ニュースをチ ェックしてください。

ChatGPT APIで2023年4月以降の情報を取得したい場合は、Web上を検索して得た情報をChatGPTに渡す必要があります。

そのため、ChatGPT APIとBingの検索結果を取得できるBing Search APIを連携をさせて、最新の情報からChatGPTのレスポンスを出力させたいと考えました。

Function calling

ChatGPT APIとBing Search APIを連携させる方法として、今回はFunction callingという機能を使用したいと思います。

Function callingとは、ユーザーからの入力に応じて実行する関数を選択し、実行のための引数を生成する機能です。

これだけでは少しわかりづらいため、今回のケースに合わせてもう少し詳しく解説をしていきます。

今回実現したい内容は「モデルが学習していない情報を聞かれた場合に、Web上を検索して最新の情報を取得して回答をして欲しい」です。

モデルが学習している情報を聞かれた場合は、Web上を検索する必要がありません。

Function callingを利用することで、Web上を検索する必要があるか、ある場合はどのような検索ワードを使用すればいいか、その判断をChatGPTにさせる事ができます。

実際のコード

まずは実際に作成したプログラムのコードを記載します。

from openai import OpenAI
from bs4 import BeautifulSoup
from dotenv import load_dotenv
import json
import os
import requests

load_dotenv()

# APIクライアントの初期化
client = OpenAI()

subscription_key = os.environ.get("BING_SUBSCRIPTION_KEY")
assert subscription_key

search_url = os.environ.get("BING_SEARCH_URL")

def get_web_content(sentence, num_results=1):

    headers = {
        "Ocp-Apim-Subscription-Key": subscription_key
    }
    params = {
        "q": sentence,                     # 検索クエリ
        "textDecorations": True,
        "textFormat": "HTML",
        "count": num_results               # 検索結果の数を指定
    }
    response = requests.get(search_url, headers=headers, params=params)
    response.raise_for_status()  # ステータスコードが200でない場合はエラーを発生させる
    search_results = response.json()  # 検索結果をJSON形式で取得

    # search_resultsからWebページのURLのみを抽出する
    if 'webPages' in search_results:
        web_pages = search_results['webPages']
        if 'value' in web_pages:
            urls = [page['url'] for page in web_pages['value']]

    all_text = ""  # 全てのテキストをこの文字列に追加

    # 各URLからHTMLを取得し、<body>タグ内のテキストを抽出
    for url in urls:
        response = requests.get(url)
        response.raise_for_status()  # ステータスコードが200でない場合にエラーを発生させる

        html_content = response.content.decode('utf-8')

        # HTMLの解析
        soup = BeautifulSoup(html_content, 'html.parser')
        body = soup.find('body')

        if body:
            # <body>内のすべてのテキストを抽出し、HTMLタグを除去
            body_text = body.get_text(separator=' ', strip=True)
            all_text += body_text + "\n\n"  # テキストを全体のテキストに追加
        else:
            print(f"No <body> tag found for URL: {url}")

    if all_text:
        message = f"以下は「{sentence}」に関する情報です。正確な情報を取得してください。\n{all_text}"
        completion = client.chat.completions.create(
            model="gpt-4-turbo",
            messages=[
                {"role": "user", "content": message}
            ]
        )

        return completion.choices[0].message.content
    else:
        return "No text was extracted from the provided URLs."

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_web_content",
            "description": "引数の文章でWeb上を検索して正確な情報を返す関数。",
            "parameters": {
                "type": "object",
                "properties": {
                    "sentence": {
                        "type": "string",
                        "description": "検索する文章。",
                    },
                },
                "required": ["sentence"],
            },
        },
    }
]

# 初期システムメッセージの設定
messages = [
    {"role": "system", "content": "あなたは優秀なアシスタントです。聞かれた情報を学習していない場合はWeb上を検索して正確な情報を返します。"}
]

while True:
    # キーボードからユーザー入力を受け取る
    user_input = input("あなたの質問を入力してください (終了するには 'quit' と入力): ")
    if user_input.lower() == 'quit':
        break

    # ユーザーメッセージを追加
    messages.append({"role": "user", "content": user_input})

    # ChatGPTモデルに問い合わせ
    completion = client.chat.completions.create(
        model="gpt-4-turbo",
        messages=messages,
        tools=tools,
        tool_choice="auto"
    )

    assistant_response = completion.choices[0].message
    tool_calls = assistant_response.tool_calls
 
    if tool_calls:
        
        # すべてのtool_callsを処理してリストにまとめる
        tool_calls_list = []
        for tool_call in tool_calls:
            tool_call_id = tool_call.id
            tool_call_type = tool_call.type
            tool_call_function = tool_call.function

            tool_calls_list.append({
                "id": tool_call_id,
                "type": tool_call_type,
                "function": {
                    "name": tool_call_function.name,
                    "arguments": tool_call_function.arguments
                }
            })

        # tool_callsの全情報をmessagesに一度に追加
        messages.append({
            "role": "assistant",
            "tool_calls": tool_calls_list
        })

        # ツールがget_web_contentである場合、その関数を実行する
        for tool_call in tool_calls_list:
            if tool_call['function']['name'] == "get_web_content":
                arguments = json.loads(tool_call['function']['arguments'])
                sentence = arguments['sentence']
                web_content = get_web_content(sentence, num_results=1)
                messages.append({"tool_call_id": tool_call['id'], "role": "tool", "name": tool_call['function']['name'], "content": web_content})

        # すべての変更をAPIに送信して結果を取得
        completion = client.chat.completions.create(
            model="gpt-4-turbo",
            messages=messages
        )
        assistant_response = completion.choices[0].message.content
        print("Assistant:", assistant_response)
        messages.append({"role": "assistant", "content": assistant_response})
    else:
        assistant_response = assistant_response.content
        print("Assistant:", assistant_response)

        messages.append({"role": "assistant", "content": assistant_response})

コード内でFunction callingに関わる部分を解説していきます。

# ChatGPTモデルに問い合わせ
completion = client.chat.completions.create(
    model="gpt-4-turbo",
    messages=messages,
    tools=tools,
    tool_choice="auto"
)

ChatGPTへのリクエストを送る際、新たにtoolstool_choiceという引数を追加しています。

toolsにはモデルが呼び出す関数の詳細を指定します。

今回は以下のようにtoolsの値を作成しました。

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_web_content",
            "description": "引数の文章でWeb上を検索して正確な情報を返す関数。",
            "parameters": {
                "type": "object",
                "properties": {
                    "sentence": {
                        "type": "string",
                        "description": "検索する文章。",
                    },
                },
                "required": ["sentence"],
            },
        },
    }
]

nameに関数名、descriptionに関数の説明、parametersに引数の情報を記載します。

tool_choiceはChatGPTに対してどの関数を使用するかを指定することが出来ます。

今回は入力プロンプトから関数を使用するかを判断してほしいため、autoにしました。

必ず関数を使用して欲しい場合は、toolsで指定した関数名をtool_choiceに記載します。

ChatGPTが入力プロンプトから関数を使用すると判断したかは、以下のtool_callsの値から確認できます。

    assistant_response = completion.choices[0].message
    tool_calls = assistant_response.tool_calls

関数を使用しない場合はNone, 使用する場合は以下のような値を取得できます。

[ChatCompletionMessageToolCall(id='call_7nqZgFn8fS8ob18eaDUCgvr7', function=Function(arguments='{"sentence":"2024年1月にDAMカラオケで一番歌われた曲"}', name='get_web_content'), type='function')]

値を確認すると、ChatGPTがget_web_content関数を使用する必要があると判断したこと、引数(検索ワード)として「2024年1月にDAMカラオケで一番歌われた曲」を生成したことがわかります。

ここで取得した値(id, type, function)をtool_callsとして、roleはassistantで以下のようにChatGPTの入力プロンプトとして追加しています。

        # すべてのtool_callsを処理してリストにまとめる
        tool_calls_list = []
        for tool_call in tool_calls:
            tool_call_id = tool_call.id
            tool_call_type = tool_call.type
            tool_call_function = tool_call.function

            tool_calls_list.append({
                "id": tool_call_id,
                "type": tool_call_type,
                "function": {
                    "name": tool_call_function.name,
                    "arguments": tool_call_function.arguments
                }
            })

        # tool_callsの全情報をmessagesに一度に追加
        messages.append({
            "role": "assistant",
            "tool_calls": tool_calls_list
        })

その後、get_web_content関数を実行してWeb上の検索結果を取得しています。

関数内では以下の処理を行っています。

  • 引数の検索ワードでWeb検索を行い、上位1件のURLを取得
  • URLからHTMLファイルを取得し、body要素内のテキストを取得
  • テキストをChatGPTの入力として使用し、情報を抽出
  • ChatGPTからの出力をそのまま戻り値として使用

ChatGPTへは以下のようにリクエストを送っています。

        message = f"以下は「{sentence}」に関する情報です。正確な情報を取得してください。\n{all_text}"
        completion = client.chat.completions.create(
            model="gpt-4-turbo",
            messages=[
                {"role": "user", "content": message}
            ]
        )

get_web_content関数内で呼び出したChatGPTは、以下のようなレスポンスを返しました。

「2024年1月にDAMカラオーで最も歌われた曲」のランキングは以下の通りです:

1. **Vaundy** - 「怪獣の花唄」 (DAMリクエストNo. 5277-09)
2. **YOASOBI** - 「アイドル」 (DAMリクエストNo. 1278-49)
3. **優里** - 「ドライフラワー」 (DAMリクエストNo. 6844-05)
4. **あいみょん** - 「マリーゴールド」 (DAMリクエストNo. 5171-36)
5. **高橋洋子** - 「残酷な天使のテーゼ」 (DAMリクエストNo. 1896-04)
6. **tuki.** - 「晩餐歌」 (DAMリクエストNo. 5476-97)
7. **Ado** - 「唱」 (DAMリクエストNo. 1128-29)
8. **ポルノグラフィティ** - 「サウダージ」 (DAMリクエストNo. 6167-17)
9. **Mrs. GREEN APPLE** - 「サウダージ」 (DAMリクエストNo. 1314-68)
10. **菅田将暉** - 「さよならエレジー」 (DAMリクエストNo. 6052-97)

このランキングは、2024年1月1日から1月31日までの期間に全機種を対象に集計されました。ただし、緊急事態宣言など の情勢により休業している店舗を除く店舗を対象としています。これは第一興商の通信カラオケDAMによって調べられた データです。

この出力を、関数の戻り値として使用します。

ChatGPTへは以下のように入力プロンプトとして追加しています。

messages.append({"tool_call_id": tool_call['id'], "role": "tool", "name": tool_call['function']['name'], "content": web_content})

roleにはtoolを指定します。

tool_call_idには予めChatGPTの出力から取得した値(id=’call_7nqZgFn8fS8ob18eaDUCgvr7′)、nameには使用した関数名(get_web_content)を指定します。

contentには関数の戻り値を指定します。

入力プロンプトの配列に関数の実行結果を追加したら、再度ChatGPTに対してリクエストを送ります。

ChatGPTからの返答は以下の通りです。

Assistant: 2024年1月にDAMカラオケで最も歌われた曲はVaundyの「怪獣の花唄」です。

ランキングから1位の結果のみを正しく取得している事がわかります。

おわりに

今回はChatGPT APIとBing Search APIの連携方法として、Function calling機能を使用する方法を記事にしました。

次回は大規模言語モデル用のライブラリであるLangChainを使用し、ChatGPTとAPIを連携する方法を記事にしたいと思います。

← 前の投稿

次の投稿 →

PythonでChatGPT APIを使ってみる 第1回 基本的な使い方

コメントを残す