こんにちは、igaです。
最近は気温の上下が大きいので、服装選びが大変ですね。
今回は、Azure OpenAI Servce Assistants APIを使ってみました。
Azure OpenAI Servce Assistants APIに横浜市の人口データを投入して、人口の増減がどう推移しているのか自動で分析させてみました。
Azure OpenAI Servce Assistants API
Azure OpenAI Servce Assistants APIとは
Azure OpenAI Servce Assistants APIは、2024年4月現在パブリックプレビューとして利用できる機能です。
Azure OpenAI Servce Assistants API(以降、Assistantsと表記します)により、Azure OpenAI Servceに独自データを投入して、投入したデータに対してユーザーからの問い合わせの回答、コードインタープリターによる分析、Function callingによる独自処理を実施することができます。
Azure OpenAI Servce Assistants APIのポイント
Azure OpenAI Servce Assistants APIを利用するには、以下の条件があります。
項目 | 値 |
利用可能なリージョン | オーストラリア東部、 米国東部2、 スウェーデン中部 |
利用可能なモデル | gpt-35-turbo(0613) gpt-35-turbo(1106) ※1 gpt-4(0613)、gpt-4(1106) |
※1 米国東部2リージョンでは利用することができません。
※2024年4月15日現在の条件です。
Assistantsで利用可能なデータファイルの拡張子は、以下のページを参照してください。
learn.microsoft.com
利用手順
Assistantsを準備する
Assistantsを利用するため、 Azureポータル からAzure OpenAI Service環境を作成します。
Azure OpenAI Service環境の利用方法については、 過去の記事 を参照してください。
Assistantsの利用手順は、こちらを参照してください。
learn.microsoft.com
Azure OpenAI Studio にアクセスして、「アシスタント(プレビュー)」メニューから、Assistantsの設定画面に移動します。
「アシスタントのセットアップ」に必要な内容を入力します。
入力する項目のポイントは以下の通りです。
入力項目 | 値 |
関数 | Function callingの関数定義をJSON形式で指定します |
コードインタープリター | 「ファイル」に独自データを指定する場合は、ONにする必要があります |
今回、ファイルには横浜市の人口動態のCSVファイルを使用しました。
関数については、 過去の記事 でも解説した通り、呼び出す関数とその引数を特定するために必要な情報をJSON形式で指定します。
関数自体の定義を指定するのではない点を注意してください。
必要事項を入力したら、Saveボタンを押して入力情報を保存します。
Assisntantsの動作を確認する
Azure OpenAI Studioのチャット画面から、今回定義したAssistantsとチャットを行います。まずは与えた人口動態データから平均人口増加数を分析させてみました。
Assistantsに渡したCSVファイルにどのような列が入っているか、こちらで説明する必要はありません。すべて勝手に読み取って解析までやってくれます。
「10年単位の人口増加数の平均値を求めて」というユーザーの入力に対して、内部でコードインタープリターが動作して、「10年単位の人口増加数の平均値」を回答してくれます。2020年代は2020年から2023年までの人口増加数が減少しているため、マイナスの値になっています。
Assistantsからの回答に対して、グラフ化してほしい、という追加の要望を出します。
グラフを生成するのと合わせて、ダウンロード可能な形式にしてくれました。
ダウンロードしたファイルは以下のようになっています。
AssistantsでFunction callingを利用する
関数定義を入力する
Assistantsに、関数の定義を追加します。
「関数の追加」をクリックすると、関数定義の入力ダイアログが開くので、関数定義のJSONを入力します。
関数定義の入力ダイアログで「保存」ボタンをおしてダイアログが閉じた後、忘れずにアシスタントのセットアップで「Save」ボタンによりAssistantsに関数定義を反映させます。
今回の定義内容は以下の通りです。
こちらの関数では、前年と今年の人口の増減比率により、「HIGH」「NORMAL」「LOW」というラベルを決定します。
{ "name": "diff_label", "description": "The label for the population increase is 'HIGH' if the population increase is 10% or more compared to the previous year, 'LOW' if the population increase is -10% or less, and 'NORMAL' for any other cases.", "parameters": { "type": "object", "properties": { "prev_population_increase": { "type": "number", "description": "Number of population increases in the previous year." }, "current_population_increase": { "type": "number", "description": "Number of population increases this year." } }, "required": [ "prev_population_increase", "current_population_increase" ] } }
Function callingを確認する
チャット画面から、上で定義したFunction callingが必要となるように、「2000年の人口増加数に対してラベルをつけて」というメッセージを入力します。
Assistantsが呼び出す関数と引数を特定してくれます。
関数を呼び出した結果を、Assistantsの応答に対して入力します。
そのままでは回答を返してくれなかったので、回答をお願いしたところFunction callingの結果を含めて回答してくれました。
APIによるAssistantsの操作
Azure OpenAI Studioのプレイグラウンドではなく、APIを使ってAssisntantsに要求を出して結果を受け取る方法を検証します。
APIの使い方については、以下の内容を参考にしました。
learn.microsoft.com
APIを操作するPythonのプログラムは以下のようになります。
Assistantsへのリクエストの送信や、応答の解析などの処理については、以下のプログラムを参考にしました。
Function callingで指定する関数については、JSONで定義した挙動になるように実装しました。
import json import os import time from pathlib import Path from typing import Optional from openai import AzureOpenAI def create_message(client, thread_id, role, content, message_id=None): """Assistantsのメッセージを作成する Args: client (AzureOpenAI): Azure OpenAIのクライアント thread_id (str): スレッドID role (str): メッセージのロール content (str): メッセージ内容 message_id (str): メッセージID """ if client is None: print("Client is required.") return None if thread_id is None: print("ThreadID 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) return client.beta.threads.messages.create(thread_id=thread_id, role=role, content=content) except Exception as ex: print(ex) return None def wait_run_finish(client, thread_id, run_id): """実行結果の終了を待機する Args: client (AzureOpenAI): Azure OpenAIのクライアント thread_id (str): スレッドID run_id (str): 実行ID """ if (client is None and thread_id is None) or run_id is None: print("Client, Thread ID and Run ID are required.") return print("wait run finish.") wait = 30 # 待機時間(秒) try: # 一定回数、状態確認を行う for cnt in range(20): run = client.beta.threads.runs.retrieve( thread_id=thread_id, run_id=run_id) print(f"Poll {cnt}: {run.status}") 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: # Function callingが必要とAssisntantsが判断したので、指定された関数を実行する 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 ex: print(ex) def retrieve_and_print_messages(client, thread_id, verbose, out_dir=None): """スレッド内のメッセージリストを取得して、結果を出力する Args: client (AzureOpenAI): Azure OpenAIのクライアント thread_is (str): スレッドID verbose (bool): 詳細表示の要否 out_dir (str): 画像ファイルの出力先フォルダ Returns list: メッセージのリスト """ 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 message_data in reversed(messages.data): if prev_role == "assistant" and message_data.role == "user" and verbose: print("------ \n") for message_content in message_data.content: # Check if valid text field is present in the mc object if message_content.type == "text": txt_val = message_content.text.value # Check if valid image field is present in the mc object elif message_content.type == "image_file": image_data = client.files.content( message_content.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 / \ (message_content.image_file.file_id + ".png") with image_path.open("wb") as f: f.write(image_data.read()) if verbose: if prev_role == message_data.role: print(txt_val) else: print(f"{display_role[message_data.role]}:\n{txt_val}") prev_role = message_data.role return messages except Exception as e: print(e) return None def get_label(prev_population_increase, current_population_increase): """前年からの増減の比率により、ラベルを返す Args: prev_population_increase (int): 前年の数値 current_population_increase (int): 今年の数値 Returns: dict: 今年の数値が前年比+10%以上ならば"HIGH"、今年の数値が前年比-10%以下ならば"LOW"、それ以外は"NORMAL" """ if current_population_increase >= prev_population_increase * 1.1: label = "HIGH" elif current_population_increase <= prev_population_increase * 0.9: label = "LOW" else: label = "NORMAL" return json.dumps({"label": label}) available_functions = {"diff_label": get_label} client = AzureOpenAI( api_key=os.getenv("AZURE_OPENAI_API_KEY"), api_version="2024-02-15-preview", azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT") ) assistant_id = os.getenv("AZURE_OPENAI_ASSISTANT_ID") assistant_list = client.beta.assistants.list() # スレッドの作成 thread = client.beta.threads.create() # メッセージの作成 first_message = create_message( client, thread.id, "user", "2000年の人口増加数について、前年との比較のラベルをつけてください。") # メッセージをAzure OpenAIに送信する run = client.beta.threads.runs.create( thread_id=thread.id, assistant_id=assistant_id) # Azure OpenAIからの応答を待つ wait_run_finish(client, thread.id, run.id) # 応答内容を出力する retrieve_and_print_messages(client, thread.id, True)
送信するメッセージは、Function callingが必要になる内容を送信します。
このプログラムを実行した結果は以下のようになります。プレイグラウンドでやった時と同じように、Function callingを使ったラベル付けした結果を得られています。
wait run finish. Poll 0: in_progress Poll 1: in_progress Poll 2: requires_action Poll 3: completed CONVERSATION: User query: 2000年の人口増加数について、前年との比較のラベルをつけてください。 Assistant response: 2000年の人口増加数は前年比で高い増加(10%以上の増加)となっており、ラベルは「HIGH」となります。
実行結果に 「ラベルは「HIGH」となります」というメッセージが出力されているので、Function callingが必要とAssistantsが判断してプログラムで実行した結果を使って、Assistantsが応答を返していることが確認できます。
APIによるファイルのダウンロード
APIを使ってAssistantsに要求するメッセージで、ファイルのダウンロードを確認します。
先ほど提示したPythonのプログラムで、作成するメッセージ内容を以下のように修正します。
# メッセージの作成 first_message = create_message( client, thread.id, "user", "10年単位での、平均人口増加数をグラフにして、画像ファイルにしてください。")
このプログラムを実行すると、以下のように画像ファイルがダウンロード可能な形式の結果が返ってきます。
プログラム中の`retrieve_and_print_messages()`で、ファイル情報を保存しています。
CONVERSATION: User query: 10年単位での、平均人口増加数をグラフにして、画像ファイルにしてください。 Assistant response: データには和暦の年と西暦の年、そして3種類の人口増加数が含まれています。ここでは「人口増加数[人]」の列を使用して、10年 単位での平均人口増加数を計算し、グラフにして画像ファイルとして保存します。まずは各10年ごとの期間に分けて平均を計算しましょう。 It seems there was an issue with plotting the data. Let me try again to calculate the average population increase per decade and create the graph. It seems there was an issue with plotting the data. Let me try again to calculate the average population increase per decade and create the graph. I have successfully calculated the average population increase per decade and created a bar graph. The graph has been saved as an image file. You can download the image using the link below: [Download the image of the average population increase per decade](sandbox:/mnt/data/average_population_increase_per_decade.png)
ダウンロードした画像は以下の通りです。
10年単位の平均人口増加数が棒グラフになっています。
まとめ
今回は、Assistantsの利用方法について確認しました。
過去の記事で、Function callingや独自データの利用方法を検証しましたが、その時にはデータの準備など手順を踏む必要がありました。
Assistantsでは、データ準備の手順が簡略化されているため、簡単なチャットシステムを用意することができるようになりました。
Acroquest Technologyでは、キャリア採用を行っています。
- ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
- Elasticsearch等を使ったデータ収集/分析/可視化
- マイクロサービス、DevOps、最新のOSSやクラウドサービスを利用する開発プロジェクト
- 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
www.wantedly.com