Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

Azure OpenAI Service の Assistants API でデータ分析

こんにちは、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月現在パブリックプレビューとして利用できる機能です。

learn.microsoft.com

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日現在の条件です。

learn.microsoft.com

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ファイルを使用しました。

www.city.yokohama.lg.jp


関数については、 過去の記事 でも解説した通り、呼び出す関数とその引数を特定するために必要な情報を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で定義した挙動になるように実装しました。

github.com

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