2024年8月2日金曜日

Google GeminiでサポートされたContext Cachingを使ってみる

Google GeminiでサポートされたContext Cachingを使ってコンテンツをキャッシュし、マルチターンのチャットを行うAPEXアプリケーションを作ってみます。

作成したアプリケーションは以下のように動作します。

Files APIを呼び出してファイルをGeminiにアップロードし、それを含めたコンテンツをキャッシュします。最後にキャッシュしたコンテンツを含めて、チャットします。

生成AIの出力を受けるのにストリーミングが使えないと、待ち時間が長く感じます。


Google GeminiのAPIを呼び出す処理は、ほぼすべてパッケージUTL_GOOGLE_GEMINI_CONTEXT_CACHINGに実装しています。このパッケージにコードは、本記事の末尾に添付しています。

作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/sample-google-gemini-context-caching.zip

アプリケーションを実行するために、Google Gemini APIにアクセスするためのAPIキーが必要です。

Google AI StudioにアクセスしAPIキーを取得します。Get API keyをクリックし、表示される指示に従ってAPIキーを作成します。


作成したAPIキーはGoogle Cloudのプロジェクトに紐づけられます。プランについては要確認です。Vertex AIからの利用ではないのでフリーのプランが適用されると考えていたら、しっかり課金されていました。


取得したAPIキーを使って、APEXのWeb資格証明を作成します。

ワークスペース・ユーティリティWeb資格証明を開きます。

名前Google Gemini API Key静的IDGOOGLE_GEMINI_API_KEYとします。認証タイプHTTPヘッダーを選択します。Google Gemini APIのAPIキーはHTTPヘッダーまたは問合せ文字列として渡すことができます。今回はHTTPヘッダーとして渡します。

資格証明名としてx-goog-api-key資格証明シークレットとしてAPIキーを設定します。

ここで作成したWeb資格証明GOOGLE_GEMINI_API_KEYを、APEX_WEB_SERVICE.MAKE_REST_REQUESTの引数p_credential_static_idに与えることにより、Gemini APIの呼び出しを認証します。


パッケージUTL_GOOGLE_GEMINI_CONTEXT_CACHINGの作成と、APEXアプリケーションのインポートを行います。

いつもの記事ではAPEXアプリケーションの作成手順を紹介していますが、今回は手順が長くなりすぎるため、実装についてポイントを絞って説明します。

Google Geminiにアップロードしたファイルの一覧とキャッシュされたコンテンツの一覧は、RESTデータ・ソースより参照するようにしています。

ファイルの一覧を取得するRESTデータ・ソースGoogle Gemini Filesキャッシュされたコンテンツの一覧はGoogle Gemini Cached Contentsとして作成されています。


ファイルの一覧は、以下のエンドポイントURLを呼び出して取得しています。

https://generativelanguage.googleapis.com/v1beta/files

操作としてGET以外にPOST(データベース・アクション:行の挿入)およびDELETE行の削除)を追加しています。これはページ作成ウィザードにより、このRESTデータ・ソースソースとしたフォーム付き対話モード・レポートを作成する際に、作成ボタンと削除ボタンを自動生成させるために追加しています。実際の処理は別途プロセスを作成して実装するため、作成や削除の実体となる設定は行なっていません。

パラメータとしてpageSize=100を固定値として与えています。Gemini APIのページングの仕組みに、Oracle APEXのRESTデータ・ソースは対応していません。今回は扱えるファイル数を100に制限しました。ページングに対応するには、RESTデータ・ソース・プラグインを自作するか、パイプライン表関数として実装しなおす必要があります。

ファイルの一覧はアプリケーション内からAPEX_EXEC.OPEN_REST_SOURCE_QUERYを使って呼び出すため、RESTデータ・ソース静的IDを設定しています。


Googleのドキュメントに明確に記述されていないようですが、リソースFileのdisplayNameに日本語を含むことができないようです。


そのため、displayNameにはUTF-8をエスケープした値を設定し、一覧に元に戻した値をdisplayNameとして返すようにデータ・プロファイルを構成しています。

データ・プロファイルとしてDISPLAY_NAME_ENCDISPLAY_NAMEを作成しています。


DISPLAY_NAME_ENC列タイプデータソースセレクタdisplayNameなので、REST APIの呼び出しにより取得された値になります。この列名をDISPLAY_NAME_ENCとします。設定共通オフにすることで、フォームのページを作成するときにページ・アイテムの作成対象から外します。


DISPLAY_NAMEは一覧に表示する列になります。列タイプSQL式ソースSQL式として以下を記述します。

utl_url.unescape(DISPLAY_NAME_ENC,'AL32UTF8')

設定共通オンにして、この名前でページ・アイテムが作成されるようにします。


キャッシュされたコンテンツの一覧は、以下のエンドポイントURLを呼び出して取得しています。

https://generativelanguage.googleapis.com/v1beta/cachedContents

ファイルの一覧と同様の理由により、操作としてPOSTDELETEを追加しています。それ以外の特別な設定は行なっていません。


ファイルを一覧する対話モード・レポートのページと、ファイルのアップロードと削除を行うフォームのページは、ページ作成ウィザードを使って生成しています。ソースとしてRESTデータ・ソースGoogle Gemini Filesを選択しています。


対話モード・レポートのページはウィザードによって生成されたページをそのまま使用しています。フォームについては、Google Gemini APIを呼び出すように変更します。

GeminiのFiles APIを呼び出してアップロードするファイルを選択するページ・アイテムを追加しています。

識別名前P3_FILEタイプファイルのアップロードです。ストレージタイプとして表APEX_APPLICATION_TEMP_FILESを選択します。クライアント側のブラウザよりAPEXのデータベースにファイルをアップロードしたのち、そのファイルをGeminiに送信するまでを、ひとつのリクエストで実行します。そのため、ファイルをパージするタイミングリクエストの終わりを選択します。セッション・ステートストレージリクエストごと(メモリーのみ)です。

サーバー側の条件を設定し、アイテムP3_NAMENULLのとき、つまりファイルの作成としてフォームが開かれたときに限り、このページ・アイテムを表示するようにします。

P3_FILE以外のページ・アイテムはP3_NAMEがNULLではないときに表示するように、サーバー側の条件を設定しています。


ファイルのアップロードと削除を行うプロセスとして、UploadDeleteを作成しています。

プロセスUploadでは、UTL_GOOGLE_GEMINI_CONTEXT_CACHING.UPLOAD_FILESを呼び出しています。引数p_file_namesにはページ・アイテムP3_FILEを割り当てています。


プロセスDeleteではUTL_GOOGLE_GEMINI_CONTEXT_CACHING.DELETE_FILEを呼び出しています。


コンテンツのキャッシュについても、フォーム付き対話モード・レポートのページをページ生成ウィザードを使って作成しています。ソースとしてRESTデータ・ソースGoogle Gemini Cached Contentsを指定しています。


キャッシュされたコンテンツを一覧する対話モード・レポートは、RESTデータ・ソースが返す値をそのまま一覧しています。

フォームには、コンテンツのキャッシュに必要なパラメータを入力するページ・アイテムを追加しています。

追加したページ・アイテムはP5_TTLP5_SYSTEM_INSTRUCTIONP5_TEXTP5_SELECTED_FILESです。P5_SELECTED_FILESはキャッシュする対象となる複数のファイルを保持します。ファイルを選択するために、RESTデータ・ソースGoogle Gemini FilesをソースとしたContent RowsのリージョンSelect Filesを作成し、そのリージョンの行選択タイプ複数選択にしています。


この他にページ・アイテムP5_TOTAL_TOKENSとボタンESTIMATE_TOKENSを作成しています。Google Geminiのコンテキストキャッシュの最小サイズは32769で、これよりトークンのサイズが小さいとコンテンツのキャッシュは失敗します。API呼び出しはステータスコード400を返します。

そのため、キャッシュする対象のトークン数を求めます。結果をP5_TOTAL_TOKENSに返します。

ボタンESTIMATE_TOKENSを押したときに以下のコードを実行しています。動的アクションからGemini APIを呼び出すと不規則にORA-1841が返されることがありました。HTTP2の永続接続が影響していると想定して、APIの呼び出し前に必ず永続接続を0にする対処を含めています。今のところ、この対処を入れた後にORA-1841は発生していません。



ボタンCREATEを押したときに実行されるプロセスCreateでは、UTL_GOOGLE_GEMINI_CONTEXT_CACHING.CREATE_CACHEを呼び出しています。


ボタンDELETEを押したときに実行されるプロセスDeleteでは、UTL_GOOGLE_GEMINI_CONTEXT_CACHING.DELETE_CACHEを呼び出しています。


チャットはホーム・ページに実装しています。


チャットを初期化するボタンINITを作成し、ブレッドクラムのリージョンに配置しています。


ボタンINITを押したときに実行されるプロセスInitで、APEXコレクションCHATを初期化しています。

apex_collection.create_or_truncate_collection('CHAT');


ページ・アイテムP1_CACHED_CONTENTで、Gemini APIのgenerateContentにcachedContentとして含めるコンテントを選択します。LOVとして共有コンポーネントCACHED_CONTENTS_LOVを選択します。


RESTデータ・ソースをソースとするLOVは共有コンポーネントとしてのみ作成できます。

CACHED_CONTENTS_LOVはRESTデータ・ソースのGoogle Gemini Cached Contentsをデータ・ソースとしています。


ユーザーによるメッセージを入力するページ・アイテムとしてP1_MESSAGEを作成します。概ねどの生成AIでもマークダウンを解釈するため、タイプとしてMarkdownエディタを選択しています。

Google GeminiのgenerateContentを呼び出すボタンSUBMITを作成します。


チャット履歴は対話モード・レポートで表示します。ソースのSQL問合せとして以下を記述します。
select seq_id, c001, clob001, n001, n002, n003, n004 from apex_collections where collection_name = 'CHAT'

ソートについては、対話モード・レポートの設定でSEQ_IDの降順で表示しています。


メッセージが表示される列CLOB001については、タイプリッチ・テキストに変更し、設定の書式マークダウンを選択しています。


N001promptTokenCountN002candidatesTokenCountN003totalTokenCountN004cachedContentTokenCountの数値が表示されています。列のヘッダーを変更すると、より分かりやすくなるでしょう。

ボタンSUBMITを押したときに実行されるプロセスChatでは、以下のコードを実行します。


送信するメッセージをAPEXコレクションCHATに追加し、それまでの履歴を含めてGemini APIのgenerateContentを呼び出します。受信したmodelの返答をAPEXコレクションに追記し、一往復のやりとりが完了します。


今回の記事は以上になります。

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


UTL_GOOGLE_GEMINI_CONTEXT_CACHING:パッケージ定義


UTL_GOOGLE_GEMINI_CONTEXT_CACHING: パッケージ本体