虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

Assistants APIのベクターストアで大規模コード解析に挑戦!

こんにちは。虎の穴ラボ エンジニアの古賀です!

今回は、OpenAIの『Assistants API』を業務(ソースコードの調査や工数の見積もり)に活用した事例を紹介します。

はじめに

これを試してみようと思った経緯としては、業務で大量のPHPのソースコードファイルについて、PHPのバージョンアップに伴う作業工数の見積もりを行うことになりました。

すべて人手で調査をするととても時間が掛かることが予想されたので、その際にPythonのプログラムとAssistants APIのFile Searchを使って、ソースコードファイルの仕様や概算の必要工数を出力するCLIのプログラムを作成し、ソースコードリーディングや見積もりに役立てることができるかどうかを検証してみました。

本記事では最初にAssistants APIについて紹介し、その後にサンプルプログラムを紹介します。

Assistants APIとは?

Assistants APIは、OpenAIが提供するベータ版APIの1つで、AIアシスタントの開発を行うためのAPIです。OpenAPIのChat Completions APIと比べると以下のような大きな特徴があります。

  • AIアシスタントとのチャット履歴を、スレッドやユーザーセッションごとに保持できる
  • AIアシスタントへ渡すコンテキスト情報として、ファイルのアップロードやベクターストアが利用できる

ちなみにサポートされているファイル形式はこのページに記載があります。

Assistants APIを使うとAIアシスタントの開発で手間がかかる機能があらかじめ用意されているため、開発を容易に行うことができます。ただし、UIは提供されていないため、必要な場合は以前のブログで紹介させていただいたサンプルコードの「openai-assistants-quickstart」を使うと開発を効率化できます。

(今回はCLIプログラムのみで、このサンプルコードは使用しません。)

Assistants APIの料金

Assistants APIの料金は、以下の要素によって決まります。

  • OpenAI APIの利用料金
  • + 1セッションあたり 0.03ドル
  • + ベクターストレージ 1日あたり 0.10 ドル / GB(1GBまでは無料)

OpenAI APIの利用料にさらにセッションあたりとベクターストレージの料金が加算されるため、利用する際には必ず試算してください。本記事の手順の中にファイルサイズから簡易的に試算するPythonのコードも含んでいます。

0. 前提条件と事前準備

  • Python、Pipenvがインストール済みであること
  • OpenAI APIキーを取得済みであること

OpenAI APIキーは、.envファイルに以下のように記述してください。

OPENAI_API_KEY=sk-1234567890abcdef1234567890abcdef

1. 環境構築

最初にopenaiのPythonライブラリをインストールします。Assistants APIはこのライブラリを使って利用します。

$ pipenv install openai

2. ベクターストアの料金試算

次のプログラムを使って、ベクターストアの料金を簡易的に試算します。

import os

# PHPファイルの検索対象ディレクトリ
BASE_PATH = "/path/to/php/files"

def get_php_files_size(directory):
    total_size = 0
    for root, _, files in os.walk(directory):
        for file in files:
            if file.endswith(".php"): # PHPファイルのみを対象
                file_path = os.path.join(root, file)
                total_size += os.path.getsize(file_path)
    return total_size

# PHPファイルのサイズを合計
total_size_bytes = get_php_files_size(BASE_PATH)
total_size_mb = total_size_bytes / (1024 * 1024)  # バイトをMB単位に変換
total_size_gb = total_size_mb / 1024  # MBをGB単位に変換

print(f"Total size of PHP files: {total_size_bytes} bytes")
print(f"Total size of PHP files: {total_size_mb:.2f} MB")
print(f"Total size of PHP files: {total_size_gb:.2f} GB")

vector_store_cost_per_gb_day = 0.10  # $ per GB per day
estimated_cost = total_size_gb * vector_store_cost_per_gb_day
print(f"Estimated cost per day for storing PHP files: ${estimated_cost:.2f}")

このプログラムを実行すると、PHPファイルのサイズとベクターストアの料金が表示されます。

$ python check-vector-price.py
Total size of PHP files: 26570404 bytes
Total size of PHP files: 25.34 MB
Total size of PHP files: 0.02 GB
Estimated cost per day for storing PHP files: $0.00

今回は、PHPファイルのサイズが0.02GBであるため、ベクターストアの料金は0.00ドルとなりました。かなり大きいサイズのファイルを大量にアップロードしない限りはあまり料金がかからないようです。

3. ベクターストアへのアップロード

次にアプリケーション内に含まれるすべてのPHPファイルをPythonのプログラムで一括でベクターストアにアップロードし、アシスタントが必要に応じて参照できるようにします。また、アップロードに時間が掛かるため、途中で失敗しても再開できる仕組みを用意しました。(ネットワークの状況によってはアップロードに時間がかかることがあるため)

再開する場合は、RESUME_PATHに再開するファイルのパスとVECTOR_STORE_IDにベクターストアのIDを指定してください。(ベクターストアのIDはOpenAI APIのStorageコンソールをみると確認できます。)

import os
from openai import OpenAI

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

# PHPファイルの検索対象ディレクトリ
BASE_PATH = "/path/to/php/files"

# アップロードを再開するための基準パス(指定がなければ None に設定)
RESUME_PATH = None

# ベクターストアの名前
VECTOR_STORE_NAME = "php-files"
VECTOR_STORE_ID = None  # 既存のベクターストアを使用する場合はID("vs_〜")を指定

# VECTOR_STORE_IDの値がある場合は、そのベクターストアを使用
vector_store = None
if VECTOR_STORE_ID:
  vector_store = client.beta.vector_stores.retrieve(vector_store_id=VECTOR_STORE_ID)
  print(f"Using existing vector store: {vector_store.name}")
else:
  # 新しいベクターストアを作成
  vector_store = client.beta.vector_stores.create(name=VECTOR_STORE_NAME)

# PHPファイルを再帰的に検索してリストを作成
def find_php_files(directory):
    php_file_paths = []
    for root, _, files in os.walk(directory):
        for file in files:
            if file.endswith(".php"):
                php_file_paths.append(os.path.join(root, file))
    return php_file_paths

# PHPファイルのリストを取得
php_files = find_php_files(BASE_PATH)

# アップロード済みファイル数のカウンター
uploaded_count = 0
total_files = len(php_files)  # 全ファイル数

# フラグの初期化
resume_upload = RESUME_PATH is None

# 各ファイルストリームのアップロード処理を行い、エラーまたは空ファイルはスキップ
for file_path in php_files:
    try:
        # 再開ポイントまでスキップ
        if not resume_upload:
            if file_path == RESUME_PATH:
                resume_upload = True
            else:
                uploaded_count += 1
                print(f"Skipping file: {file_path}")
                continue

        # ファイルサイズをチェック
        if os.path.getsize(file_path) == 0:
            uploaded_count += 1
            print(f"Skipping empty file: {file_path}")
            continue  # ファイルが空の場合は次のファイルにスキップ

        # ファイルを開いてアップロード
        with open(file_path, "rb") as file_stream:
            file_batch = client.beta.vector_stores.file_batches.upload_and_poll(
                vector_store_id=vector_store.id, files=[file_stream]
            )

        # アップロード成功時にカウントと進捗の表示
        uploaded_count += 1
        progress_percentage = (uploaded_count / total_files) * 100
        print(f"Successfully uploaded {file_path} ({uploaded_count}/{total_files}, {progress_percentage:.2f}% complete)")

    except Exception as e:
        print(f"Error uploading {file_path}: {str(e)}")

print("All files processed.")

# アップロードされたファイルの最終ステータスとファイル数を表示
try:
    print("File batch upload finished with status:", file_batch.status)
    print("Number of files successfully processed:", file_batch.file_counts)
except NameError:
    print("No files were successfully uploaded.")

このプログラムを次のように実行すると、PHPファイルがベクターストアにアップロードされます。

$ pipenv run python upload-vectorstore.py

4. ソースコードの解析を行う

最後に、Assistants APIのFile Searchを使って指定したPHPファイルの解析を行います。解析したいPHPファイルの一覧はphplist.txtに改行区切りで指定し、解析結果はCSV(diagnosis_results.csv)として出力します。

より良い結果を得るためには、事前に調査した内容やアプリケーションの仕様をMarkdownで記載しておくと、AIアシスタントがより適切な回答を返してくれることがあります。また、出力されるCSVの例がないとCSVのフォーマットにしたがって回答をしてくれない可能性が高いので、事前にいくつかのPHPファイル(10個程度)で実行し、望ましい結果に近いCSVを綺麗に整形して指示文の例のところに貼り付けると精度が上がります。

(プロンプトエンジニアリングの「One-shot/Few-shot」と呼ばれる手法です)

import os
from openai import OpenAI

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

# OpenAI Assistantの指示文
sys_prompt = """
あなたはPHPに関してとても詳しいプロフェッショナルのエンジニアです。PHPのバージョンアップ(xからy)に伴う作業について、以下のCSVフォーマットで日本語で回答をしてください。
[ファイルパス],[機能や役割の要約],[参照しているライブラリの一覧],[参照している他のPHPファイルとその関数の一覧],[不明なクラスや関数参照の一覧],[アップデートが必要なライブラリやコードの箇所とその修正方法の一覧],[修正難易度:低/中/高],[修正にかかる予測工数(時間)],[その他補足]

含まれるPHPファイルについて、事前に調査した内容は下記になります。
この内容も参考にして回答してください。
(事前に調査した内容やアプリケーションの仕様が分かれば、ここにMarkdownで記載しておく)

回答はCSVフォーマットのみでそれ以外の文章は絶対に含めないでください。含められると非常に困ります。
以下はCSVの例です。

(ここに望ましい結果に近いCSVを貼り付ける。CSVのエスケープがされてなかったらエスケープする。)

以上のことを必ず踏まえて、PHPファイルをチェックしてください。
"""

# ベースパス(PHPファイルの相対パスを基にしたフルパスを形成するために使用)
BASE_PATH = "/path/to/php/files"

# 入力となるテキストファイル(PHPファイルの相対パスを1行ずつ記述)
input_file_path = "phplist.txt"

# 出力のCSVファイル
output_csv_path = "diagnosis_results.csv"

# ベクターストアのIDリスト(現状はAPIの仕様的に最大1つまで!)
vector_store_ids = ["vs_〜"]

with open(input_file_path, "r") as file:
    php_file_paths = file.read().splitlines()

# 診断用のAssistantを作成
assistant = client.beta.assistants.create(
    name="PHPファイルを解析するBot",
    instructions=sys_prompt,
    model="gpt-4o-2024-08-06",
    tools=[{"type": "file_search"}],
    tool_resources={"file_search": {"vector_store_ids": vector_store_ids}},
)

# スレッドを作成
thread = client.beta.threads.create()

# 各PHPファイルの診断結果をCSVファイルに記録
with open(output_csv_path, "wt", encoding="utf-8") as text_file:

  for relative_path in php_file_paths:
    try:
      # フルパスを形成
      full_path = os.path.join(BASE_PATH, relative_path)

      # PHPファイルの内容を読み取る
      with open(full_path, "r", encoding="utf-8") as php_file:
          php_content = php_file.read()

      # OpenAIにPHPファイルの内容を渡して診断してもらう
      # PHPファイルの内容を診断するようにプロンプトを設定
      content = f"ファイルパス:{relative_path} \n```php\n{php_content}\n```"

      # ユーザーからのメッセージをスレッドに追加
      message = client.beta.threads.messages.create(
          thread_id=thread.id,
          role="user",
          content=content
      )

      # AIの回答を取得
      run = client.beta.threads.runs.create_and_poll(
        thread_id=thread.id,
        assistant_id=assistant.id,
      )
      # 診断結果をCSVに書き込む
      if run.status == 'completed':
        messages = client.beta.threads.messages.list(
          thread_id=thread.id
        )
        print(messages.data[0].content[-1].text.value)
        text_file.write(messages.data[0].content[-1].text.value + "\n")
        text_file.flush()
    except Exception as e:
      # エラーが発生した場合、その旨をCSVに書き込む
      text_file.write(f"\nError: {str(e)}\n")
      print(f"\n\nError processing file {relative_path}: {str(e)}\n\n")

# Assistantとスレッドのクリーンアップ
client.beta.assistants.delete(assistant_id=assistant.id)
client.beta.threads.delete(thread_id=thread.id)     

このプログラムを実行すると、指定したPHPファイルの解析結果がCSVファイルに出力されます。

$ pipenv run python check-code.py

以下は出力されたCSVファイルの例です

xxx.php,"画像情報を取得して表示するスクリプト。Imagickを利用して画像の幅、高さ、深度を取得しています。","Imagick","なし","なし","なし","Imagick関連の処理 → PHPが8.0以降でも有効であることを確認して、特に変更が必要ない場合はそのまま使用。
getImageDepth()の動作を確認し、必要であれば対応する方法に変更。例えば、Imagickがインストールされているか確認し、パフォーマンスへの影響がないかチェックする。",低,1,"このスクリプトはPHPのImagick拡張を利用しており、PHP 8.0に引き続き対応可能です。大きな変更やアップグレードの負担は少ないと予測されます。",
yyy.php,"S3にファイルをアップロードするスクリプト。条件により異なるバケットやACLを設定してアップロードしています。","なし","./roles/home/files/viewer_batch/batch/converter.php73/common.phpで定義されているputS3Object関数","なし","なし","AWS関連の設定(AWS_S3_BUCKET、AWS_S3_ACL等) → PHP 8.0で動作を確認し、必要ならAWS SDK for PHPのアップデート。特に指定された定数の場所、計算方法、またはその使用方法が変更されていないか再確認。putS3Object関連の変更が必要な場合、AWS SDKの新しいバージョンを使用し、変更点に合わせた修正。",中,2,"AWS SDKのバージョンによっては、コードの一部を再確認し修正が必要です。最新のライブラリバージョンでの動作確認をお勧めします。"

まとめ

今回はAssistants APIのベクターストアを使って大量のPHPファイルの解析を行い、PHPのファイルの調査や工数の見積もりのサポートをAIにさせてみました。

出力された結果にはImageMagickやAWS SDKのバージョンアップに伴う作業がどのAPIにどの程度時間がかかるか、また、どのような修正が必要かが記載されていましたし、改修にかかる予測工数や参照している他のPHPファイルや関数の一覧、機能の概要もほぼ正確に出力されていました。

内容に関しての正確性は100%ではないですが、大量のPHPファイルの解析を行う際に機能ごとにどのファイルを見るべきかの目安がついたので、かなり効率的に作業を進めることができると感じました。

思わぬ副作用としては、作成したベクターストアを使ったチャットがAssitants APIのコンソール上で可能なので、調査の際に知りたい機能があるPHPファイルの場所や内容をチャットで確認することもでき、作ってみて良かったなと感じています。

採用情報

虎の穴ラボでは一緒に働く仲間を募集中です!
この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧ください。
toranoana-lab.co.jp