Azure上のAssistants APIでBing Searchを試す
Assistants function calling with Bing Search
以下のサンプルを参考にBing Search APIs と function calling を使用して応答にBing Searchの情報を含めるアシスタントを作成します。この方法を使用することで回答に最新の情報を含めることできます。
以下のサンプルを進める前にBing Searchのリソースを作成してください。
Bing Search resource
はじめに
必要パッケージ
%pip install requests openai
Parameters
パラメーター設定を行います。Bing Searchのリソースのキーとエンドポイントを設定します。
キーはAzure Portalから取得できます。
azure_endpoint = "https://<YOUR_RESOURCE_NAME>.openai.azure.com"
api_version = "2024-02-15-preview"
aoai_api_key = "<AOAI_RESOURCE_API_KEY>"
deployment_name = "<DEPLOYMENT_NAME>"
bing_search_subscription_key = "<BING_SEARCH_SUBSCRIPTION_KEY>"
bing_search_url = "https://api.bing.microsoft.com/v7.0/search"
Bing Search APIを呼び出す関数を定義
LLM での Bing 検索 API の使用要件等は以下から確認してください。
Bing Search APIs, with your LLM.
import json
import requests
import time
from openai import AzureOpenAI
from pathlib import Path
from typing import Optional
以下のPythonコードは、指定された検索クエリに対してBing検索を実行し、その結果をリストとして返す関数を定義しています。
-
関数の定義:
-
search
関数を定義します。この関数は、検索クエリを引数として受け取り、検索結果のリストを返します。
-
-
Bing Search APIのリクエストヘッダー:
- Bing Search APIのリクエストヘッダーを定義します。このヘッダーには、Bing Search APIのサブスクリプションキーが含まれます。
-
リクエストパラメータ:
- リクエストパラメータを定義します。これには、検索クエリとテキスト装飾のオプションが含まれます。
-
Bing Search APIへのGETリクエスト:
-
requests.get
関数を使用してBing Search APIにGETリクエストを送信します。このリクエストには、Bing Search APIのURL、リクエストヘッダー、およびリクエストパラメータが含まれます。
-
-
リクエストの成功確認:
-
response.raise_for_status()
を呼び出して、リクエストが成功したかどうかを確認します。これは、リクエストが失敗した場合に例外を発生させます。
-
-
レスポンスボディの解析:
-
response.json()
を呼び出して、レスポンスボディをJSON形式で解析し、その結果をsearch_results
変数に格納します。
-
-
検索結果の処理:
- 空のリスト
output
を作成します。このリストは、検索結果を格納するためのものです。 -
search_results["webPages"]["value"]
をループして、各検索結果を処理します。各検索結果について、そのタイトル(result["name"]
)、リンク(result["url"]
)、およびスニペット(result["snippet"]
)を取得し、それらを辞書としてoutput
リストに追加します。
- 空のリスト
-
結果の返却:
-
json.dumps(output)
を呼び出して、output
リストをJSON形式の文字列に変換し、それを返します。
-
この関数は、指定された検索クエリに対してBing検索を実行し、その結果をJSON形式の文字列として返すためのものです。
def search(query: str) -> list:
"""
Perform a bing search against the given query
@param query: Search query
@return: List of search results
"""
headers = {"Ocp-Apim-Subscription-Key": bing_search_subscription_key}
params = {"q": query, "textDecorations": False}
response = requests.get(bing_search_url, headers=headers, params=params)
response.raise_for_status()
search_results = response.json()
output = []
for result in search_results["webPages"]["value"]:
output.append({"title": result["name"], "link": result["url"], "snippet": result["snippet"]})
return json.dumps(output)
- 試しにBing APIを使用してみます。
search("2024年のオリンピックはどこで開催されますか?")
以下のような出力がされるはずです。
'[{"title": "Paris 2024 Olympics - Latest News, Schedules & Results", "link": "・・・・
Assistantの作成
アシスタントに必要ないくつかの関数を事前に定義します。これらの関数はすべて最後のセルで使用します。
Web検索アシスタントを定義し、その機能に関する指示を与え、質問してみます。
def poll_run_till_completion(
client: AzureOpenAI,
thread_id: str,
run_id: str,
available_functions: dict,
verbose: bool,
max_steps: int = 10,
wait: int = 3,
) -> None:
"""
Poll a run until it is completed or failed or exceeds a certain number of iterations (MAX_STEPS)
with a preset wait in between polls
@param client: OpenAI client
@param thread_id: Thread ID
@param run_id: Run ID
@param assistant_id: Assistant ID
@param verbose: Print verbose output
@param max_steps: Maximum number of steps to poll
@param wait: Wait time in seconds between polls
"""
if (client is None and thread_id is None) or run_id is None:
print("Client, Thread ID and Run ID are required.")
return
try:
cnt = 0
while cnt < max_steps:
run = client.beta.threads.runs.retrieve(thread_id=thread_id, run_id=run_id)
if verbose:
print("Poll {}: {}".format(cnt, run.status))
cnt += 1
if run.status == "requires_action":
tool_responses = []
if (
run.required_action.type == "submit_tool_outputs"
and run.required_action.submit_tool_outputs.tool_calls is not None
):
tool_calls = run.required_action.submit_tool_outputs.tool_calls
for call in tool_calls:
if call.type == "function":
if call.function.name not in available_functions:
raise Exception("Function requested by the model does not exist")
function_to_call = available_functions[call.function.name]
tool_response = function_to_call(**json.loads(call.function.arguments))
tool_responses.append({"tool_call_id": call.id, "output": tool_response})
run = client.beta.threads.runs.submit_tool_outputs(
thread_id=thread_id, run_id=run.id, tool_outputs=tool_responses
)
if run.status == "failed":
print("Run failed.")
break
if run.status == "completed":
break
time.sleep(wait)
except Exception as e:
print(e)
poll_run_till_completion
関数は、指定されたOpenAIクライアント、スレッドID、実行IDを使って、ある実行が完了、失敗する、または指定された最大ステップ数(MAX_STEPS
)を超えるまで状態を定期的にチェックするためのポーリングメカニズムです。ポーリングの間隔はwait
パラメータで設定します。
- もしクライアント、スレッドID、実行IDのいずれかが指定されていない場合、関数はエラーメッセージを表示して終了します。
-
max_steps
の回数だけ次の処理をループします:- 実行のステータスを確認します。
verbose
がTrue
の場合、現在のポーリング回数とステータスを表示します。 - ステータスが
requires_action
の場合、指定されたアクション(必要なツールの出力を送信するなど)を実行します。 - ステータスが
failed
の場合、エラーメッセージを表示してループを終了します。 - ステータスが
completed
の場合、ループを終了します。 - 上記のいずれにも該当しない場合、指定された
wait
時間だけ待機します。 - 何らかの例外が発生した場合、その例外を表示します。
- 実行のステータスを確認します。
この関数は、長時間実行される処理や非同期タスクの完了を効率的に管理するために使用されます。
Runのライフサイクルは以下が参考になります。
def create_message(
client: AzureOpenAI,
thread_id: str,
role: str = "",
content: str = "",
file_ids: Optional[list] = None,
metadata: Optional[dict] = None,
message_id: Optional[str] = None,
) -> any:
"""
Create a message in a thread using the client.
@param client: OpenAI client
@param thread_id: Thread ID
@param role: Message role (user or assistant)
@param content: Message content
@param file_ids: Message file IDs
@param metadata: Message metadata
@param message_id: Message ID
@return: Message object
"""
if metadata is None:
metadata = {}
if file_ids is None:
file_ids = []
if client is None:
print("Client parameter is required.")
return None
if thread_id is None:
print("Thread ID is required.")
return None
try:
if message_id is not None:
return client.beta.threads.messages.retrieve(thread_id=thread_id, message_id=message_id)
if file_ids is not None and len(file_ids) > 0 and metadata is not None and len(metadata) > 0:
return client.beta.threads.messages.create(
thread_id=thread_id, role=role, content=content, file_ids=file_ids, metadata=metadata
)
if file_ids is not None and len(file_ids) > 0:
return client.beta.threads.messages.create(
thread_id=thread_id, role=role, content=content, file_ids=file_ids
)
if metadata is not None and len(metadata) > 0:
return client.beta.threads.messages.create(
thread_id=thread_id, role=role, content=content, metadata=metadata
)
return client.beta.threads.messages.create(thread_id=thread_id, role=role, content=content)
except Exception as e:
print(e)
return None
Pythonコードは、AzureのOpenAIクライアントを使用してスレッド内にメッセージを作成する関数create_message
を定義しています。
-
関数の定義:
-
create_message
関数は、OpenAIクライアント、スレッドID、メッセージの役割(ユーザーまたはアシスタント)、メッセージの内容、メッセージのファイルID、メッセージのメタデータ、メッセージIDを引数として受け取ります。
-
-
初期設定:
- メタデータとファイルIDが
None
の場合、それぞれを空の辞書と空のリストに設定します。
- メタデータとファイルIDが
-
前提条件の確認:
- クライアントとスレッドIDが指定されていることを確認します。これらが指定されていない場合、エラーメッセージを表示して関数を終了します。
-
メッセージの処理:
-
try
ブロックを開始します。このブロック内で、メッセージIDが指定されている場合、client.beta.threads.messages.retrieve
メソッドを使用してメッセージを取得します。 - ファイルIDとメタデータが指定されている場合、
client.beta.threads.messages.create
メソッドを使用して新しいメッセージを作成します。このメソッドは、スレッドID、役割、内容、ファイルID、メタデータを引数として受け取ります。 - ファイルIDのみが指定されている場合、メタデータのみが指定されている場合、どちらも指定されていない場合について、同様に
client.beta.threads.messages.create
メソッドを使用して新しいメッセージを作成します。
-
-
例外処理:
- 例外が発生した場合、その例外を表示して
None
を返します。
- 例外が発生した場合、その例外を表示して
この関数は、OpenAIのスレッド内に新しいメッセージを作成するためのものです。
def retrieve_and_print_messages(
client: AzureOpenAI, thread_id: str, verbose: bool, out_dir: Optional[str] = None
) -> any:
"""
Retrieve a list of messages in a thread and print it out with the query and response
@param client: OpenAI client
@param thread_id: Thread ID
@param verbose: Print verbose output
@param out_dir: Output directory to save images
@return: Messages object
"""
if client is None and thread_id is None:
print("Client and Thread ID are required.")
return None
try:
messages = client.beta.threads.messages.list(thread_id=thread_id)
display_role = {"user": "User query", "assistant": "Assistant response"}
prev_role = None
if verbose:
print("\n\nCONVERSATION:")
for md in reversed(messages.data):
if prev_role == "assistant" and md.role == "user" and verbose:
print("------ \n")
for mc in md.content:
# Check if valid text field is present in the mc object
if mc.type == "text":
txt_val = mc.text.value
# Check if valid image field is present in the mc object
elif mc.type == "image_file":
image_data = client.files.content(mc.image_file.file_id)
if out_dir is not None:
out_dir_path = Path(out_dir)
if out_dir_path.exists():
image_path = out_dir_path / (mc.image_file.file_id + ".png")
with image_path.open("wb") as f:
f.write(image_data.read())
if verbose:
if prev_role == md.role:
print(txt_val)
else:
print("{}:\n{}".format(display_role[md.role], txt_val))
prev_role = md.role
return messages
except Exception as e:
print(e)
return None
retrieve_and_print_messages
関数は、指定されたスレッドIDに関連するメッセージのリストを取得し、それらを表示する役割を果たします。
-
前提条件の確認:
-
client
またはthread_id
がNone
の場合、エラーメッセージを表示してNone
を返します。
-
-
スレッドのメッセージリストの取得:
- スレッドのメッセージリストを取得します。
-
メッセージの表示設定:
- メッセージの役割(ユーザーまたはアシスタント)に対応する表示名を設定します。
-
verbose
がTrue
の場合、"CONVERSATION:"と表示します。
-
メッセージの処理:
- 取得したメッセージを逆順に処理します。
- 前のメッセージがアシスタントからのもので、現在のメッセージがユーザーからのものである場合、
verbose
がTrue
の場合は"------"と表示します。 - メッセージの内容を処理します。内容がテキストの場合、そのテキストを取得します。内容が画像の場合、その画像のデータを取得し、指定された出力ディレクトリに保存します。
-
verbose
がTrue
の場合、メッセージの役割と内容(テキスト)を表示します。前のメッセージと現在のメッセージの役割が同じ場合は、内容だけを表示します。
-
例外の処理:
- 何らかの例外が発生した場合、その例外を表示して
None
を返します。
- 何らかの例外が発生した場合、その例外を表示して
-
パラメータ:
-
client
: OpenAIのクライアント -
thread_id
: スレッドID -
verbose
: 詳細表示の有無 -
output_dir
: 画像の保存先ディレクトリ
-
この関数は、上記のパラメータを使用して、スレッドのメッセージを取得し、それらを表示します。
name = "BingSearchAssistant"
instructions = """ユーザーが質問に答えるのを助けるように設計されたアシスタントです。
あなたはBing Searchを使ってWEBに問い合わせることができます。質問が最新の情報を必要とする場合や、WEBデータの恩恵を受ける可能性がある場合は、いつでもBing Searchを呼び出す必要があります。回答は日本語で答えてください。
"""
message = {"role": "user", "content": "富士山の高さは何mですか?"}
tools = [
{
"type": "function",
"function": {
"name": "search_bing",
"description": "WEBから最新の情報を得るためにBingを検索する。",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query",
}
},
"required": ["query"],
},
},
}
]
available_functions = {"search_bing": search}
verbose_output = True
client = AzureOpenAI(api_key=aoai_api_key, api_version=api_version, azure_endpoint=azure_endpoint)
assistant = client.beta.assistants.create(
name=name, description="", instructions=instructions, tools=tools, model=deployment_name
)
thread = client.beta.threads.create()
create_message(client, thread.id, message["role"], message["content"])
run = client.beta.threads.runs.create(thread_id=thread.id, assistant_id=assistant.id, instructions=instructions)
poll_run_till_completion(
client=client, thread_id=thread.id, run_id=run.id, available_functions=available_functions, verbose=verbose_output
)
messages = retrieve_and_print_messages(client=client, thread_id=thread.id, verbose=verbose_output)
出力結果
Poll 0: queued
Poll 1: in_progress
Poll 2: requires_action
Poll 3: in_progress
Poll 4: completed
CONVERSATION:
User query:
富士山の高さは何mですか?
Assistant response:
富士山の高さは3,776mです。
Bing APIを活用して回答を返してくれています。
コードの内容は、OpenAIのAzure APIを使用して、Bing Searchを活用した質問応答アシスタントを作成し、実行するものです。
-
アシスタントの定義:
- アシスタントの名前と指示を定義します。このアシスタントは、最新の情報を必要とする質問やウェブデータが役立つ質問に対して、Bing Searchを呼び出すように設計されています。
-
ユーザーからのメッセージ:
- ユーザーが「富士山の高さは何mですか?」という質問をしています。
-
使用可能なツールの定義:
- Bingを検索して最新の情報を取得する関数
search_bing
をツールとして定義しています。
- Bingを検索して最新の情報を取得する関数
-
AzureOpenAIクライアントの作成:
- AzureOpenAIクライアントを作成し、そのクライアントを使用してアシスタントを作成します。アシスタントの作成には、定義した名前、指示、ツール、およびモデル名が必要です。
-
スレッドの作成とメッセージの追加:
- アシスタントが作成されたら、スレッドを作成し、ユーザーからのメッセージをそのスレッドに追加します。
-
スレッドの実行:
- アシスタントがユーザーからのメッセージに対してアクションを実行します。この例では、アシスタントはBingを検索して富士山の高さを調べます。
-
メッセージの取得と出力:
- スレッドからメッセージを取得し、それらを出力します。これにより、アシスタントがユーザーの質問にどのように応答したかを確認することができます。
- Pollからrunライフサイクルのどこを実行しているか確認します。
Azure Portal上からも作成したアシスタントが確認できます。
また、以下のように実行することで、assistant
、messages
、run
等の詳細を確認できます。
import json
def show_json(obj):
display(json.loads(obj.model_dump_json()))
show_json(assistant)
サンプル例:
show_json(messages)
show_json(run)
まとめ
- Assistants APIを活用し、Bing Searchのデータを取り込んで、ユーザーの質問にリアルタイムで回答する方法について解説しました。
- 本記事では、Bing Search APIと機能呼び出しを組み合わせたサンプルコードを用いて、最新情報を応答に含めるプロセスを紹介。
- この技術を駆使することで、ユーザーに最新の情報を提供するアシスタントアプリを開発できます。
Discussion