2024年11月29日金曜日

Oracle APEXのアプリからOpenAIのBatch APIを呼び出す

OpenAIが2024年4月にBatch APIをリリースしています。少し時間が経ちましたが、OpenAIのBatch APIを呼び出して、Chat Completions API + Structured Outputsで文章のJSON表現を取り出す仕組みと、Embeddings APIでエンべディングを取得する仕組みを、Oracle APEXのアプリケーションとして実装してみました。OpenAI Batch APIの呼び出しは、パッケージUTL_OPENAI_BATCH_APIに実装しています。APEXアプリケーションは、そのパッケージに実装したファンクションを呼び出して動作を確認するために作成しています。

Oracle APEXでは、ほぼすべての処理をデータベースで実行していて、OpenAIのChat Completions APIやEmbeddings APIはデータベースのサーバーから呼び出されます。ブラウザからデータベースを介してChat Completions APIが呼び出されるときは、ブラウザからORDSを介したデータベースへの接続と、データベースからOpenAIのAPIサーバーへの接続の両方が、APIの処理が終わるまで維持されます。データベースはAPIの処理中は、その応答を待機して何も処理は行いませんが、セッションつまりサーバー・プロセスは占有したままになります。リソースの利用効率としては、あまり良くありません。OpenAIの新しい推論モデルo1のような、レスポンスが返されるまでに長時間かかる場合は、特に良くありません。

OpenAIのBatch APIを呼び出すことにより、この点が改善されます(注: 現時点ではo1-previewはBatch APIでは使えないので、通常のモデルでの話です)。Batch APIという名前の通り、会話の用途には使用できませんが、ドキュメントの要約、Structured Outputsを指定したドキュメントのJSON出力、エンべディングの生成などは、対話的に処理する必要がありません。Batch APIの応答は待機する必要がなく、Retrieve batchによるポーリングを行うことによりリクエストの完了を確認します。占有されるサーバー・プロセスは発生しません。また、コストも50%程度削減できるようです。削減されるコストについては、OpenAIのPricingのページを確認してください。

Batch APIのリクエストはOpenAI Files APIを使って、ファイルとしてOpenAIのストレージにアップロードする必要があります。また、結果のアウトプット・ファイルやエラーが記載されたファイルも、OpenAIのストレージに作成されます。そのために以前の記事「OpenAIのFiles APIを使ってファイルをアップロードする」で紹介している、OpenAIのFiles APIを呼び出すPL/SQLパッケージUTL_OPENAI_FILES_APIをあらかじめ作成しておきます。

今回作成したパッケージUTL_OPENAI_BATCH_APIのコードは、記事の末尾に添付します。

作成したAPEXアプリケーションのエクスポートは以下です。
https://github.com/ujnak/apexapps/blob/master/exports/sample-openai-batch-api.zip

以下より、作成したAPEXアプリケーションを紹介します。

最初にOpenAIへ発行するリクエストやレスポンスを保持する表を作成します。クイックSQLの以下のモデルを使用します。

OPENAI_BATCH_SUBMISSIONSはBatch APIで発行するリクエストを保持します。表OPENAI_BATCH_REQUESTSはそれぞれのバッチに含まれるChat Completions APIまたはEmbeddings APIのリクエストを保持します。OPENAI_BATCH_SUBMISSIONSとはSUBMISSION_IDをキーとした親子関係があります。表OPENAI_BATCH_RESPONSESはoutput_file_idで指定されたバッチの結果出力ファイルの内容をパースして、それぞれのリクエストに対応したレスポンスごとに保存します。OPENAI_BATCH_REQUESTSとはCUSTOM_IDで紐づきます。バッチの入力ファイル、出力ファイルともに改行で区切られたJSONファイル(Newline Delimited JSON - NDJSONまたはJSON LInes - JSONL)なので、OPENAI_BATCH_REQUESTSおよびOPENAI_BATCH_RESPONSESともに、1行のJSONが表の1行になります。



バッチ・リクエストの発行と結果の取得は、アプリケーションのホーム・ページに実装しています。パッケージUTL_OPENAI_BATCH_APIには、おおむねボタン名に対応したプロシージャまたはファンクションが含まれています。ボタンのクリックで、パッケージに実装されたそれらの処理が呼び出されます。

Createボタンを押すとダイアログが開きます。


Endpointとして/v1/chat/completionsまたは/v1/embeddingsのどちらかを選択します。Completion Windowも指定します。以下では24hを指定しています。

Create Batchをクリックして、表OPENAI_BATCH_SUBMISSIONSに1行挿入します。


Createの横にDeleteボタンがあります。選択されているSubmission IDの行を表OPENAI_BATCH_SUBMISSIONSから削除する際に使用しますが、そのSUBMISSION_IDに紐づくリクエストが表OPENAI_BATCH_REQUESTSに存在する場合はエラーが発生します。


Endpointとして/v1/chat/completionsを指定した場合は、Append Chatのボタンが表示されます。

今回は以下の情報を設定しています。Modelとしてはgpt-4o-miniSystem Messageに「あなたは日本の昔話に詳しいアシスタントです。User Messageとして、次のメッセージ「以下の物語より、登場人物とその関係についてJSON形式で表現してください。」に続けて、青空文庫より竹取物語をコピペしました。

https://www.aozora.gr.jp/cards/001072/files/48310_42692.html

Structured Outputsとして、以下のJSON Schemaを指定しています。

以上でボタンAppend Chatをクリックします。


竹取物語は長文で、レポートにエラーが発生します。


このエラーは対話モード・レポートで発生しているエラーです。リクエストは正常に表OPENAI_BATCH_REQUESTSに追加されています。

表示列からBodyを除くと、対話モード・レポートのエラーが無くなります。


同様に、リクエストにカチカチ山を追加します。

https://www.aozora.gr.jp/cards/000329/files/18377_11982.html


複数のリクエストを追加したのち、ボタンSubmit Batchをクリックします。

複数のリクエストを改行区切りのJSONでファイルにまとめて、そのファイルをOpenAIのストレージにアップロードしたのち、Batch APIのCreate Batchリクエストを発行します。


Create Batchの発行直後はStatusvalidatingになるようです。

Update Batchをクリックすると、発行済みのバッチ・リクエストのステータスを更新します。


バッチが処理中の場合はin_progressになります。statusが取りえる状態は、OpenAIのBatch APIのガイドに一覧されています。


バッチ処理が完了するとStatuscompletedになります。statusの種類にはfailedがありますが、これはvalidationでの失敗から遷移する状態で、バッチ処理が失敗していてもstatusはcompletedになります。失敗しているのはバッチに含まれている、個々のリクエストであって、その場合はバッチ処理自体は成功していると見做されているようです。


アプリケーションにはList batchのページが含まれています。このページでは、Batch APIのList batchリクエストを発行し、今までに発行したバッチ処理を一覧します。

この中にError File Idの項目があります。バッチに含まれているリクエストでエラーが発生している場合、そのエラー・メッセージはError Fileに書き込まれます。Error Fileが作成されていると、そのError FileにError File IDが割り当てられます。

エラーの内容はError File IDを指定して、OpenAIのストレージからError Fileをダウンロードすることで確認できます。

パッケージUTL_OPENAI_BATCH_APIに含まれるプロシージャdownload_batch_responseでは、output_file_idで指定できるバッチの出力ファイルを表OPENAI_BATCH_SUBMISSIONSの列RESPONSE_FILEに保存するとともに、error_file_idがあればError Fileを列ERROR_FILEに保存します。


バッチ処理が完了した後、ボタンGet Resultをクリックすることにより、OpenAIのストレージに保存されたOutput FileとError File(もしあれば)を表OPENAI_BATCH_SUBMISSIONSへダウンロードします。また、続けてプロシージャparse_batch_responseを呼び出し、バッチのレスポンスに含まれるChat Completions APIとしてのレスポンスに加えて、そのレスポンスに含まれる最初のメッセージも取り出し、表OPENAI_BATCH_RESPONSESの列BODYFIRST_RESPONSEに保存します。


OPENAI_BATCH_RESPONSESの対話モード・レポートの編集アイコンをクリックすると、フォーム形式でレスポンスを確認できます。


竹取物語のJSON出力として、以下が得られました。
{
  "characters" :
  [
    {
      "name" : "竹取の翁",
      "role" : "主人公",
      "relations" :
      {
        "妻" : "竹取の翁の妻",
        "娘" : "赫映姫(かぐやひめ)"
      }
    },
    {
      "name" : "竹取の妻",
      "role" : "翁の妻",
      "relations" :
      {
        "夫" : "竹取の翁",
        "娘" : "赫映姫(かぐやひめ)"
      }
    },
    {
      "name" : "赫映姫",
      "role" : "翁の養女、月の姫",
      "relations" :
      {
        "父" : "竹取の翁",
        "母" : "竹取の妻"
      }
    },
    {
      "name" : "石造皇子",
      "role" : "求婚者の一人",
      "relations" :
      {
        "姫" : "赫映姫(かぐやひめ)"
      }
    },
    {
      "name" : "車持皇子",
      "role" : "求婚者の一人",
      "relations" :
      {
        "姫" : "赫映姫(かぐやひめ)"
      }
    },
    {
      "name" : "阿倍御主人",
      "role" : "求婚者の一人",
      "relations" :
      {
        "姫" : "赫映姫(かぐやひめ)",
        "大臣" : "大納言大伴御行の友人"
      }
    },
    {
      "name" : "大納言大伴御行",
      "role" : "求婚者の一人",
      "relations" :
      {
        "姫" : "赫映姫(かぐやひめ)",
        "友人" : "阿倍御主人"
      }
    },
    {
      "name" : "中納言石上麻呂",
      "role" : "求婚者の一人",
      "relations" :
      {
        "姫" : "赫映姫(かぐやひめ)"
      }
    }
  ]
}
カチカチ山のJSON出力です。
{
  "characters" :
  [
    {
      "name" : "おじいさん",
      "role" : "物語の主人公。おばあさんと二人三脚で生活している。たぬきにだまされて悲劇に見舞われる。",
      "relations" :
      {
        "おばあさん" : "妻",
        "たぬき" : "敵",
        "白うさぎ" : "友人"
      }
    },
    {
      "name" : "おばあさん",
      "role" : "おじいさんの妻。たぬきに騙されて命を落とす。",
      "relations" :
      {
        "おじいさん" : "夫",
        "たぬき" : "敵"
      }
    },
    {
      "name" : "たぬき",
      "role" : "物語の悪役。おじいさんの畑を荒らし、おばあさんを欺き、最終的におじいさんを裏切る。",
      "relations" :
      {
        "おじいさん" : "敵",
        "おばあさん" : "敵",
        "白うさぎ" : "競争相手"
      }
    },
    {
      "name" : "白うさぎ",
      "role" : "おじいさんの友人。たぬきの復讐を助けようとする。",
      "relations" :
      {
        "おじいさん" : "友人",
        "たぬき" : "敵"
      }
    }
  ]
}
すごい。

Any sufficiently advanced technology is indistinguishable from magic.」

生のRequest File(バッチのリクエストとなったファイル - OpenAIのAPIではInput File)、Response File(バッチの出力ファイル - OpenAIのAPIではOutput File)、Error Fileは、Batchesのページに表OPENAI_BATCH_SUBMISSIONSの対話モード・レポートを作成してあり、そのレポートからダウンロードできるようになっています。


Embeddingsの場合は、Endpointとして/v1/embeddingsを設定したエントリを作成し、ボタンAdd Embeddingsをクリックしてリクエストを追加します。


バッチ処理のリクエストの発行、確認および結果のダウンロードはChat Completions APIのときと同様にボタンSubmit BatchUpdate StatusGet Resultをクリックして実行します。

Embeddingsに関しては、Output Fileのそれぞれの行のJSONからbodyを取り出すところまでは処理しています。bodyからエンべディングを取り出す実装は含めていません。


OpenAIのBatch APIを使うことにより、以下が可能になります。
  1. データベースを効果的に利用できます。
  2. OpenAIへ支払う費用が削減(50%オフなので半減)できます。
  3. Structured Outputsにより非定型文書よりグラフとして扱えるデータを生成したり、セマンティック検索で使用するエンべディングを生成することができます。それらの形式のデータは、直接Oracle Database 23aiのグラフやベクトルの機能で活用できます。
今回の記事は以上になります。

Oracle APEXのアプリケーション作成の参考になれば幸いです。