Google GeminiでサポートされたContext Cachingを使ってコンテンツをキャッシュし、マルチターンのチャットを行うAPEXアプリケーションを作ってみます。
作成したアプリケーションは以下のように動作します。
Files APIを呼び出してファイルをGeminiにアップロードし、それを含めたコンテンツをキャッシュします。最後にキャッシュしたコンテンツを含めて、チャットします。
生成AIの出力を受けるのにストリーミングが使えないと、待ち時間が長く感じます。
作成した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、静的IDはGOOGLE_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を設定しています。
そのため、displayNameにはUTF-8をエスケープした値を設定し、一覧に元に戻した値をdisplayNameとして返すようにデータ・プロファイルを構成しています。
データ・プロファイルの列としてDISPLAY_NAME_ENCとDISPLAY_NAMEを作成しています。
列DISPLAY_NAMEは一覧に表示する列になります。列タイプはSQL式、ソースのSQL式として以下を記述します。
utl_url.unescape(DISPLAY_NAME_ENC,'AL32UTF8')
ファイルのアップロードと削除を行うプロセスとして、UploadとDeleteを作成しています。
コンテンツのキャッシュについても、フォーム付き対話モード・レポートのページをページ生成ウィザードを使って作成しています。ソースとしてRESTデータ・ソースのGoogle Gemini Cached Contentsを指定しています。
キャッシュされたコンテンツを一覧する対話モード・レポートは、RESTデータ・ソースが返す値をそのまま一覧しています。
設定の共通はオンにして、この名前でページ・アイテムが作成されるようにします。
キャッシュされたコンテンツの一覧は、以下のエンドポイントURLを呼び出して取得しています。
https://generativelanguage.googleapis.com/v1beta/cachedContents
ファイルの一覧と同様の理由により、操作としてPOSTとDELETEを追加しています。それ以外の特別な設定は行なっていません。
ファイルを一覧する対話モード・レポートのページと、ファイルのアップロードと削除を行うフォームのページは、ページ作成ウィザードを使って生成しています。ソースとしてRESTデータ・ソースのGoogle Gemini Filesを選択しています。
対話モード・レポートのページはウィザードによって生成されたページをそのまま使用しています。フォームについては、Google Gemini APIを呼び出すように変更します。
GeminiのFiles APIを呼び出してアップロードするファイルを選択するページ・アイテムを追加しています。
識別の名前はP3_FILE、タイプはファイルのアップロードです。ストレージのタイプとして表APEX_APPLICATION_TEMP_FILESを選択します。クライアント側のブラウザよりAPEXのデータベースにファイルをアップロードしたのち、そのファイルをGeminiに送信するまでを、ひとつのリクエストで実行します。そのため、ファイルをパージするタイミングにリクエストの終わりを選択します。セッション・ステートのストレージはリクエストごと(メモリーのみ)です。
サーバー側の条件を設定し、アイテムP3_NAMEがNULLのとき、つまりファイルの作成としてフォームが開かれたときに限り、このページ・アイテムを表示するようにします。
P3_FILE以外のページ・アイテムはP3_NAMEがNULLではないときに表示するように、サーバー側の条件を設定しています。
プロセスUploadでは、UTL_GOOGLE_GEMINI_CONTEXT_CACHING.UPLOAD_FILESを呼び出しています。引数p_file_namesにはページ・アイテムP3_FILEを割り当てています。
プロセスDeleteではUTL_GOOGLE_GEMINI_CONTEXT_CACHING.DELETE_FILEを呼び出しています。
フォームには、コンテンツのキャッシュに必要なパラメータを入力するページ・アイテムを追加しています。
追加したページ・アイテムはP5_TTL、P5_SYSTEM_INSTRUCTION、P5_TEXT、P5_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を作成し、ブレッドクラムのリージョンに配置しています。
apex_collection.create_or_truncate_collection('CHAT');
送信するメッセージをAPEXコレクションCHATに追加し、それまでの履歴を含めてGemini APIのgenerateContentを呼び出します。受信したmodelの返答をAPEXコレクションに追記し、一往復のやりとりが完了します。
ページ・アイテムP1_CACHED_CONTENTで、Gemini APIのgenerateContentにcachedContentとして含めるコンテントを選択します。LOVとして共有コンポーネントのCACHED_CONTENTS_LOVを選択します。
RESTデータ・ソースをソースとするLOVは共有コンポーネントとしてのみ作成できます。
CACHED_CONTENTS_LOVはRESTデータ・ソースのGoogle Gemini Cached Contentsをデータ・ソースとしています。
Google GeminiのgenerateContentを呼び出すボタンSUBMITを作成します。
チャット履歴は対話モード・レポートで表示します。ソースのSQL問合せとして以下を記述します。
select seq_id, c001, clob001, n001, n002, n003, n004 from apex_collections where collection_name = 'CHAT'
ソートについては、対話モード・レポートの設定でSEQ_IDの降順で表示しています。
メッセージが表示される列CLOB001については、タイプをリッチ・テキストに変更し、設定の書式にマークダウンを選択しています。
列N001にpromptTokenCount、N002にcandidatesTokenCount、N003にtotalTokenCount、N004にcachedContentTokenCountの数値が表示されています。列のヘッダーを変更すると、より分かりやすくなるでしょう。
ボタンSUBMITを押したときに実行されるプロセスChatでは、以下のコードを実行します。