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は発生していません。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare | |
l_count pls_integer; | |
begin | |
/* | |
* ランダムに発生するORA-1841の原因としてHTTP2の接続の影響が考えられる。 | |
* そのため、永続接続が存在する場合、一旦接続を閉じてからリクエストを発行するようにする。 | |
*/ | |
l_count := sys.utl_http.get_persistent_conn_count; | |
if l_count > 0 then | |
sys.utl_http.close_persistent_conns( host => 'generativelanguage.googleapis.com' ); | |
end if; | |
/* | |
* 対応終了。 | |
*/ | |
:P5_TOTAL_TOKENS := utl_google_gemini_context_caching.estimate_token_count( | |
p_text => :P5_TEXT | |
,p_selected_files => :P5_SELECTED_FILES | |
,p_credential_static_id => :G_CREDENTIAL | |
); | |
end; |
ボタン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では、以下のコードを実行します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare | |
l_contents json_array_t := json_array_t(); | |
l_content json_object_t; | |
l_parts json_array_t; | |
l_part json_object_t; | |
l_model_response clob; | |
l_prompt_token_count number; | |
l_candidates_token_count number; | |
l_total_token_count number; | |
l_cached_content_token_count number; | |
begin | |
/* c001 = role, clob001 = text */ | |
apex_collection.add_member( | |
p_collection_name => 'CHAT' | |
,p_c001 => 'user' | |
,p_clob001 => :P1_MESSAGE | |
); | |
/* | |
* 送信するcontentsオブジェクトを作成する。 | |
*/ | |
for c in ( | |
select c001, clob001 from apex_collections where collection_name = 'CHAT' | |
order by seq_id asc | |
) | |
loop | |
l_content := json_object_t(); | |
l_parts := json_array_t(); | |
l_part := json_object_t(); | |
l_part.put('text', c.clob001); | |
l_parts.append(l_part); | |
l_content.put('parts', l_part); | |
l_content.put('role', c.c001); | |
l_contents.append(l_content); | |
end loop; | |
-- | |
utl_google_gemini_context_caching.generate_content( | |
p_contents => l_contents | |
,p_cached_content => :P1_CACHED_CONTENT | |
,p_credential_static_id => :G_CREDENTIAL | |
,p_model_response => l_model_response | |
,p_prompt_token_count => l_prompt_token_count | |
,p_candidates_token_count => l_candidates_token_count | |
,p_total_token_count => l_total_token_count | |
,p_cached_content_token_count => l_cached_content_token_count | |
); | |
apex_collection.add_member( | |
p_collection_name => 'CHAT' | |
,p_c001 => 'model' | |
,p_clob001 => l_model_response | |
,p_n001 => l_prompt_token_count | |
,p_n002 => l_candidates_token_count | |
,p_n003 => l_total_token_count | |
,p_n004 => l_cached_content_token_count | |
); | |
end; |
今回の記事は以上になります。
Oracle APEXのアプリケーション作成の参考になれば幸いです。
完
UTL_GOOGLE_GEMINI_CONTEXT_CACHING:パッケージ定義
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
create or replace package "UTL_GOOGLE_GEMINI_CONTEXT_CACHING" as | |
C_UPLOAD_URL constant varchar2(100) := 'https://generativelanguage.googleapis.com/upload/v1beta/files'; | |
C_DELETE_URL constant varchar2(100) := 'https://generativelanguage.googleapis.com/v1beta/'; | |
C_CACHE_URL constant varchar2(100) := 'https://generativelanguage.googleapis.com/v1beta/cachedContents'; | |
C_GENERATE_URL constant varchar2(100) := 'https://generativelanguage.googleapis.com/v1beta/#MODEL#:generateContent'; | |
C_TOKEN_URL constant varchar2(100) := 'https://generativelanguage.googleapis.com/v1beta/#MODEL#:countTokens'; | |
C_MODEL_DEFAULT constant varchar2(30) := 'models/gemini-1.5-flash-001'; | |
C_TTL_DEFAULT constant varchar2(10) := '3600s'; | |
procedure upload_file( | |
p_file_name in varchar2 | |
,p_mime_type in varchar2 | |
,p_content in blob | |
,p_credential_static_id in varchar2 | |
,p_response out clob | |
); | |
procedure delete_file( | |
p_name in varchar2 | |
,p_credential_static_id in varchar2 | |
); | |
procedure upload_files( | |
p_file_names in varchar2 | |
,p_credential_static_id in varchar2 | |
); | |
procedure create_cache( | |
p_display_name in varchar2 | |
,p_model in varchar2 default C_MODEL_DEFAULT | |
,p_ttl in varchar2 default C_TTL_DEFAULT | |
,p_credential_static_id in varchar2 | |
,p_text in clob | |
,p_selected_files in varchar2 | |
,p_system_instruction in clob default null | |
,p_total_token_count out number | |
); | |
procedure delete_cache( | |
p_name in varchar2 | |
,p_credential_static_id in varchar2 | |
); | |
procedure generate_content( | |
p_contents in json_array_t | |
,p_model in varchar2 default C_MODEL_DEFAULT | |
,p_cached_content in varchar2 | |
,p_credential_static_id in varchar2 | |
,p_model_response out clob | |
,p_prompt_token_count out number | |
,p_candidates_token_count out number | |
,p_total_token_count out number | |
,p_cached_content_token_count out number | |
); | |
function estimate_token_count( | |
p_text in clob | |
,p_selected_files in varchar2 | |
,p_model in varchar2 default C_MODEL_DEFAULT | |
,p_credential_static_id in varchar2 | |
) return number; | |
end "UTL_GOOGLE_GEMINI_CONTEXT_CACHING"; | |
/ |
UTL_GOOGLE_GEMINI_CONTEXT_CACHING: パッケージ本体
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
create or replace package body "UTL_GOOGLE_GEMINI_CONTEXT_CACHING" as | |
/** | |
Google Gemini APIのmedia.uplooadを呼び出す。 | |
Ref: https://ai.google.dev/api/files#method:-media.upload | |
*/ | |
function upload_file_meta( | |
p_file_name in varchar2 | |
,p_mime_type in varchar2 | |
,p_content in blob | |
,p_credential_static_id in varchar2 | |
) return varchar2 | |
as | |
l_file_json json_object_t := json_object_t(); | |
l_request_json json_object_t := json_object_t(); | |
l_content_length number; | |
l_request clob; | |
l_request_length number; | |
l_response clob; | |
l_upload_url varchar2(400); | |
e_api_call_failed exception; | |
-- pragma exception_init(e_api_call_failed, -20001); | |
begin | |
-- Limit to ASCII characters for display_name. | |
l_file_json.put('display_name', utl_url.escape(p_file_name, false, 'AL32UTF8')); | |
l_request_json.put('file', l_file_json); | |
l_request := l_request_json.to_clob(); | |
l_content_length := dbms_lob.getlength(p_content); | |
l_request_length := length(l_request); | |
/* | |
* ファイルのメタデータを登録する。 | |
*/ | |
apex_web_service.clear_request_headers(); | |
apex_web_service.set_request_headers('X-Goog-Upload-Protocol' ,'resumable' ,p_reset => false); | |
apex_web_service.set_request_headers('X-Goog-Upload-Command' ,'start' ,p_reset => false); | |
apex_web_service.set_request_headers('X-Goog-Upload-Header-Content-Length',l_content_length ,p_reset => false); | |
apex_web_service.set_request_headers('X-Goog-Upload-Header-Content-Type' ,p_mime_type ,p_reset => false); | |
apex_web_service.set_request_headers('Content-Type' ,'application/json',p_reset => false); | |
apex_web_service.set_request_headers('Content-Length' ,l_request_length ,p_reset => false); | |
l_response := apex_web_service.make_rest_request( | |
p_url => C_UPLOAD_URL | |
,p_http_method => 'POST' | |
,p_body => l_request | |
,p_credential_static_id => p_credential_static_id | |
); | |
if apex_web_service.g_status_code <> 200 then | |
apex_debug.info('upload.meta failed: %s, %s, %s', | |
apex_web_service.g_status_code, l_response, l_request); | |
raise e_api_call_failed; | |
end if; | |
/* | |
* ファイル本体をアップロードするURLを取り出す。 | |
*/ | |
for h in 1 .. apex_web_service.g_headers.count | |
loop | |
if lower(apex_web_service.g_headers(h).name) = 'x-goog-upload-url' then | |
l_upload_url := apex_web_service.g_headers(h).value; | |
exit; | |
end if; | |
end loop; | |
return l_upload_url; | |
end upload_file_meta; | |
/** | |
ファイル本体をGeminiのストレージにアップロードする。 | |
APIの仕様としては、ファイルを分割して複数回に分けてアップロードを実施できるようにしているが、テストはしていない。 | |
APEXアプリケーションでファイルを一度にアップロードできているので、DBからGoogleのストレージへのアップロードも | |
一度のAPI呼び出しで完了するはず。 | |
*/ | |
procedure upload_file_body( | |
p_upload_url in varchar2 | |
,p_content in blob | |
,p_credential_static_id in varchar2 | |
,p_response out clob | |
,p_command in varchar2 default 'upload, finalize' | |
,p_offset in number default 0 | |
) | |
as | |
l_content_length number; | |
e_api_call_failed exception; | |
-- pragma exception_init(e_api_call_failed, -20002); | |
begin | |
l_content_length := dbms_lob.getlength(p_content); | |
apex_web_service.clear_request_headers(); | |
apex_web_service.set_request_headers('Content-Length' ,l_content_length,p_reset => false); | |
apex_web_service.set_request_headers('X-Goog-Upload-Offset' ,p_offset ,p_reset => false); | |
apex_web_service.set_request_headers('X-Goog-Upload-Command',p_command ,p_reset => false); | |
p_response := apex_web_service.make_rest_request( | |
p_url => p_upload_url | |
,p_http_method => 'POST' | |
,p_body_blob => p_content | |
,p_credential_static_id => p_credential_static_id | |
); | |
if apex_web_service.g_status_code <> 200 then | |
apex_debug.info('upload.body failed: %s, %s, %s', | |
apex_web_service.g_status_code, p_response, l_content_length); | |
raise e_api_call_failed; | |
end if; | |
end upload_file_body; | |
/** | |
ファイルのメタデータの登録と本体のアップロードを一度で行う。 | |
*/ | |
procedure upload_file( | |
p_file_name in varchar2 | |
,p_mime_type in varchar2 | |
,p_content in blob | |
,p_credential_static_id in varchar2 | |
,p_response out clob | |
) | |
as | |
l_upload_url varchar2(400); | |
begin | |
l_upload_url := upload_file_meta( | |
p_file_name => p_file_name | |
,p_mime_type => p_mime_type | |
,p_content => p_content | |
,p_credential_static_id => p_credential_static_id | |
); | |
upload_file_body( | |
p_upload_url => l_upload_url | |
,p_content => p_content | |
,p_credential_static_id => p_credential_static_id | |
,p_response => p_response | |
); | |
end upload_file; | |
/** | |
ファイルのアップロードのページ・アイテムの値を受け取って、指定されている | |
複数のファイルをAPEX_APPLICATION_TEMP_FILESより取り出して | |
Geminiのストレージにアップロードする。 | |
*/ | |
procedure upload_files( | |
p_file_names in varchar2 | |
,p_credential_static_id in varchar2 | |
) | |
is | |
l_response clob; | |
begin | |
for r in ( | |
select * from apex_application_temp_files | |
where name in ( | |
select column_value from apex_string.split(p_file_names, ':') | |
) | |
) | |
loop | |
upload_file( | |
p_file_name => r.filename | |
,p_mime_type => r.mime_type | |
,p_content => r.blob_content | |
,p_credential_static_id => p_credential_static_id | |
,p_response => l_response | |
); | |
end loop; | |
end upload_files; | |
/** | |
Google Geminiのオブジェクトを削除する。 | |
FileとcachedContentの両方に対応しているが、呼び出しはdelete_file, delete_cacheとして呼び出す。 | |
*/ | |
procedure delete_object( | |
p_name in varchar2 | |
,p_credential_static_id in varchar2 | |
) | |
as | |
l_response clob; | |
e_api_call_failed exception; | |
-- pragma exception_init(e_api_call_failed, -20003); | |
begin | |
apex_web_service.clear_request_headers(); | |
l_response := apex_web_service.make_rest_request( | |
p_url => C_DELETE_URL || p_name | |
,p_http_method => 'DELETE' | |
,p_credential_static_id => p_credential_static_id | |
); | |
if apex_web_service.g_status_code <> 200 then | |
apex_debug.info('delete failed: %s, %s', | |
apex_web_service.g_status_code, l_response); | |
raise e_api_call_failed; | |
end if; | |
end delete_object; | |
/** | |
Google Geminiのストレージにあるファイルを削除する。 | |
Ref: https://ai.google.dev/api/files#method:-files.delete | |
*/ | |
procedure delete_file( | |
p_name in varchar2 | |
,p_credential_static_id in varchar2 | |
) | |
as | |
begin | |
delete_object( | |
p_name => p_name | |
,p_credential_static_id => p_credential_static_id | |
); | |
end delete_file; | |
/** | |
CachedContentを作成する。 | |
Ref: https://ai.google.dev/api/caching#method:-cachedcontents.create | |
Ref: https://ai.google.dev/api/caching#v1beta.cachedContents | |
*/ | |
procedure create_cache_internal( | |
p_display_name in varchar2 | |
,p_model in varchar2 | |
,p_ttl in varchar2 | |
,p_credential_static_id in varchar2 | |
,p_contents in json_array_t default null | |
,p_tools in json_array_t default null | |
,p_system_instruction in json_object_t default null | |
,p_tool_config in json_object_t default null | |
,p_create_time out timestamp | |
,p_update_time out timestamp | |
,p_usage_metadata out json_object_t | |
,p_expire_time out timestamp | |
) | |
as | |
l_request_json json_object_t := json_object_t(); | |
l_request clob; | |
l_response clob; | |
l_response_json json_object_t; | |
e_api_call_failed exception; | |
-- pragma exception_init(e_api_call_failed, -20003); | |
begin | |
/* | |
* キャッシュされるコンテンツを設定する。 | |
*/ | |
if p_contents is not null then | |
l_request_json.put('contents', p_contents); | |
end if; | |
if p_tools is not null then | |
l_request_json.put('tools', p_tools); | |
end if; | |
if p_system_instruction is not null then | |
l_request_json.put('systemInstruction', p_system_instruction); | |
end if; | |
if p_tool_config is not null then | |
l_request_json.put('toolConfig', p_tool_config); | |
end if; | |
/* | |
* APIとしてはOptionalだが、このアプリではキャッシュされたコンテンツの識別に使うためdisplayNameは必ず指定する。 | |
*/ | |
l_request_json.put('displayName', p_display_name); | |
l_request_json.put('model', p_model); | |
l_request_json.put('ttl', p_ttl); -- デフォルトは3600sとしている。 | |
l_request := l_request_json.to_clob(); | |
apex_web_service.clear_request_headers(); | |
apex_web_service.set_request_headers('Content-Type', 'application/json', p_reset => false); | |
l_response := apex_web_service.make_rest_request( | |
p_url => C_CACHE_URL | |
,p_http_method => 'POST' | |
,p_body => l_request | |
,p_credential_static_id => p_credential_static_id | |
); | |
if apex_web_service.g_status_code <> 200 then | |
apex_debug.info('cachedContents.create failed: %s, %s, %s', | |
apex_web_service.g_status_code, l_response, l_request); | |
raise e_api_call_failed; | |
end if; | |
l_response_json := json_object_t(l_response); | |
p_create_time := l_response_json.get_timestamp('createTime'); | |
p_update_time := l_response_json.get_timestamp('updateTime'); | |
p_usage_metadata := l_response_json.get_object('usageMetadata'); | |
p_expire_time := l_response_json.get_timestamp('expireTime'); | |
end create_cache_internal; | |
/** | |
contentsオブジェクト(配列)を作成する。 | |
*/ | |
procedure create_contents_object( | |
p_text in clob | |
,p_mime_types in apex_t_varchar2 | |
,p_file_uris in apex_t_varchar2 | |
,p_contents out json_array_t | |
) | |
as | |
l_contents json_array_t; | |
l_content json_object_t; | |
l_parts json_array_t; | |
l_part json_object_t; | |
l_file_data json_object_t; | |
begin | |
l_parts := json_array_t(); | |
if p_text is not null then | |
l_part := json_object_t(); | |
l_part.put('text', p_text); | |
l_parts.append(l_part); | |
end if; | |
if p_mime_types is not null then | |
for i in 1 .. p_mime_types.count | |
loop | |
l_part := json_object_t(); | |
l_file_data := json_object_t(); | |
l_file_data.put('mimeType', p_mime_types(i)); | |
l_file_data.put('fileUri', p_file_uris(i)); | |
l_part.put('fileData', l_file_data); | |
l_parts.append(l_part); | |
end loop; | |
end if; | |
l_contents := json_array_t(); | |
l_content := json_object_t(); | |
l_content.put('parts', l_parts); | |
l_content.put('role','user'); | |
l_contents.append(l_content); | |
p_contents := l_contents; | |
end create_contents_object; | |
/** | |
選択されたファイルからmimeTypeとfileUriを取り出す。 | |
*/ | |
procedure select_files( | |
p_selected_files in varchar2 | |
,p_mime_types out apex_t_varchar2 | |
,p_file_uris out apex_t_varchar2 | |
,p_region_static_id in varchar2 default 'GOOGLE_GEMINI_FILES' | |
) | |
as | |
l_context apex_exec.t_context; | |
l_filters apex_exec.t_filters; | |
l_columns apex_exec.t_columns; | |
l_mimetype_idx pls_integer; | |
l_uri_idx pls_integer; | |
l_names apex_t_varchar2; | |
l_mime_types apex_t_varchar2 := apex_t_varchar2(); | |
l_file_uris apex_t_varchar2 := apex_t_varchar2(); | |
begin | |
l_names := apex_string.split(p_selected_files, ':'); | |
-- 選択されたファイルがあるときだけ、fileDataをキャッシュに含める。 | |
if l_names.count > 0 then | |
apex_exec.add_filter( | |
p_filters => l_filters | |
,p_filter_type => apex_exec.c_filter_in | |
,p_column_name => 'NAME' | |
,p_values => l_names | |
); | |
begin | |
l_context := apex_exec.open_rest_source_query( | |
p_static_id => p_region_static_id, | |
p_filters => l_filters, | |
p_max_rows => 100 ); | |
l_uri_idx := apex_exec.get_column_position( l_context, 'URI' ); | |
l_mimetype_idx := apex_exec.get_column_position( l_context, 'MIME_TYPE' ); | |
while apex_exec.next_row( l_context ) | |
loop | |
apex_string.push(l_mime_types,apex_exec.get_varchar2( l_context, l_mimetype_idx )); | |
apex_string.push(l_file_uris, apex_exec.get_varchar2( l_context, l_uri_idx )); | |
end loop; | |
p_mime_types := l_mime_types; | |
p_file_uris := l_file_uris; | |
apex_exec.close( l_context ); | |
exception | |
when others then | |
apex_exec.close( l_context ); | |
end; | |
end if; | |
end select_files; | |
/** | |
以下の条件でキャッシュを作成する。 | |
1. contentsの配列にcontentは1つ。 | |
2. contentのroleはuser。 | |
3. textのPartを1つ。 | |
4. fileDataは複数。 | |
*/ | |
procedure create_cache( | |
p_display_name in varchar2 | |
,p_model in varchar2 default C_MODEL_DEFAULT | |
,p_ttl in varchar2 default C_TTL_DEFAULT | |
,p_credential_static_id in varchar2 | |
,p_text in clob | |
,p_selected_files in varchar2 | |
,p_system_instruction in clob default null | |
,p_total_token_count out number | |
) | |
as | |
l_contents json_array_t; | |
l_system_instruction json_object_t; | |
l_parts json_array_t; | |
l_part json_object_t; | |
l_mime_types apex_t_varchar2; | |
l_file_uris apex_t_varchar2; | |
-- out | |
l_usage_metadata json_object_t; | |
l_create_time timestamp; | |
l_update_time timestamp; | |
l_expire_time timestamp; | |
begin | |
/* | |
* 選択したファイルのmimeTypeとfileUriをRESTデータソースから取り出す。 | |
*/ | |
select_files( | |
p_selected_files => p_selected_files | |
,p_mime_types => l_mime_types | |
,p_file_uris => l_file_uris | |
); | |
/* | |
* 与えられたテキストとファイルのデータからGeminiへの入力となるcontents配列を生成する。 | |
*/ | |
create_contents_object( | |
p_text => p_text | |
,p_mime_types => l_mime_types | |
,p_file_uris => l_file_uris | |
,p_contents => l_contents | |
); | |
/* | |
* System InstructionをCLOBからContentオブジェクトに変換する。 | |
*/ | |
if p_system_instruction is not null then | |
l_system_instruction := json_object_t(); | |
l_parts := json_array_t(); | |
l_part := json_object_t(); | |
l_part.put('text', p_system_instruction); | |
l_parts.append(l_part); | |
l_system_instruction.put('parts', l_parts); | |
-- systemInstructionにroleは不要らしい。 | |
end if; | |
/* | |
* コンテンツをキャッシュする。 | |
*/ | |
create_cache_internal( | |
p_display_name => p_display_name | |
,p_model => p_model | |
,p_ttl => p_ttl | |
,p_credential_static_id => p_credential_static_id | |
,p_contents => l_contents | |
,p_system_instruction => l_system_instruction | |
,p_create_time => l_create_time | |
,p_update_time => l_update_time | |
,p_usage_metadata => l_usage_metadata | |
,p_expire_time => l_expire_time | |
); | |
if l_usage_metadata is not null then | |
p_total_token_count := l_usage_metadata.get_number('totalTokenCount'); | |
end if; | |
end create_cache; | |
/** | |
キャッシュを削除する。 | |
Ref: https://ai.google.dev/api/caching#method:-cachedcontents.delete | |
*/ | |
procedure delete_cache( | |
p_name in varchar2 | |
,p_credential_static_id in varchar2 | |
) | |
as | |
begin | |
delete_object( | |
p_name => p_name | |
,p_credential_static_id => p_credential_static_id | |
); | |
end delete_cache; | |
/** | |
cachedContentがあることを前提として、マルチターンの呼び出しを行う。 | |
Ref: https://ai.google.dev/api/generate-content#method:-models.generatecontent | |
*/ | |
procedure generate_content_internal( | |
p_contents in json_array_t | |
,p_model in varchar2 | |
,p_cached_content in varchar2 | |
,p_credential_static_id in varchar2 | |
,p_response out clob | |
) | |
as | |
l_url varchar2(200); | |
l_request_json json_object_t := json_object_t(); | |
l_request clob; | |
l_response clob; | |
e_api_call_failed exception; | |
-- pragma exception_init(e_api_call_failed, -20004); | |
begin | |
/* | |
* Cached Contentに指定したモデルと、generateContentに指定したモデルが | |
* 一致しているかどうかは確認していない。 | |
*/ | |
l_url := replace(C_GENERATE_URL, '#MODEL#', p_model); | |
l_request_json.put('contents', p_contents); | |
l_request_json.put('cachedContent', p_cached_content); | |
l_request := l_request_json.to_clob(); | |
apex_web_service.clear_request_headers(); | |
apex_web_service.set_request_headers('Content-Type', 'application/json', p_reset => false); | |
l_response := apex_web_service.make_rest_request( | |
p_url => l_url | |
,p_http_method => 'POST' | |
,p_body => l_request | |
,p_credential_static_id => p_credential_static_id | |
); | |
if apex_web_service.g_status_code <> 200 then | |
apex_debug.info('models.generateContent failed: %s, %s, %s', | |
apex_web_service.g_status_code, l_response, l_request); | |
raise e_api_call_failed; | |
end if; | |
p_response := l_response; | |
end generate_content_internal; | |
/** | |
candidates配列のオブジェクト、contentオブジェクトのparts配列、parts配列の先頭のtext(roleはmodel)を取り出して返す。 | |
*/ | |
procedure generate_content( | |
p_contents in json_array_t | |
,p_model in varchar2 | |
,p_cached_content in varchar2 | |
,p_credential_static_id in varchar2 | |
,p_model_response out clob | |
,p_prompt_token_count out number | |
,p_candidates_token_count out number | |
,p_total_token_count out number | |
,p_cached_content_token_count out number | |
) | |
as | |
l_response clob; | |
l_response_json json_object_t; | |
l_candidates json_array_t; | |
l_candidate json_object_t; | |
l_content json_object_t; | |
l_parts json_array_t; | |
l_part json_object_t; | |
l_usage_metadata json_object_t; | |
begin | |
generate_content_internal( | |
p_contents => p_contents | |
,p_model => p_model | |
,p_cached_content => p_cached_content | |
,p_credential_static_id => p_credential_static_id | |
,p_response => l_response | |
); | |
l_response_json := json_object_t(l_response); | |
l_candidates := l_response_json.get_array('candidates'); | |
l_candidate := treat(l_candidates.get(0) as json_object_t); | |
l_content := l_candidate.get_object('content'); | |
l_parts := l_content.get_array('parts'); | |
l_part := treat(l_parts.get(0) as json_object_t); | |
p_model_response := l_part.get_string('text'); | |
l_usage_metadata := l_response_json.get_object('usageMetadata'); | |
p_prompt_token_count := l_usage_metadata.get_number('promptTokenCount'); | |
p_candidates_token_count := l_usage_metadata.get_number('candidatesTokenCount'); | |
p_total_token_count := l_usage_metadata.get_number('totalTokenCount'); | |
p_cached_content_token_count := l_usage_metadata.get_number('cachedContentTokenCount'); | |
end generate_content; | |
/** | |
トークン数を数えるAPIを呼び出す。 | |
Ref: https://ai.google.dev/api/tokens#method:-models.counttokens | |
*/ | |
function count_tokens( | |
p_contents in json_array_t | |
,p_model in varchar2 default C_MODEL_DEFAULT | |
,p_credential_static_id in varchar2 | |
) return number | |
as | |
l_url varchar2(200); | |
l_request clob; | |
l_request_json json_object_t := json_object_t(); | |
l_response clob; | |
l_response_json json_object_t; | |
e_api_call_failed exception; | |
-- pragma exception_init(e_api_call_failed, -20004); | |
e_invalid_date exception; | |
pragma exception_init(e_invalid_date, -1841); | |
l_detailed_sqlerrm varchar2(32767); | |
begin | |
l_url := replace(C_TOKEN_URL, '#MODEL#', p_model); | |
l_request_json.put('contents', p_contents); | |
l_request := l_request_json.to_clob(); | |
apex_web_service.clear_request_headers(); | |
apex_web_service.set_request_headers('Content-Type', 'application/json', p_reset => false); | |
apex_web_service.g_status_code := 0; | |
begin | |
l_response := apex_web_service.make_rest_request( | |
p_url => l_url | |
,p_http_method => 'POST' | |
,p_body => l_request | |
,p_credential_static_id => p_credential_static_id | |
); | |
exception | |
-- 発生原因が不明の ORA-1841 について、特別にログを取る。 | |
when e_invalid_date then | |
l_detailed_sqlerrm := sys.utl_http.get_detailed_sqlerrm; | |
apex_debug.info('%s, %s', | |
l_detailed_sqlerrm, l_request); | |
raise; | |
end; | |
if apex_web_service.g_status_code <> 200 then | |
apex_debug.info('models.countTokens failed: %s, %s, %s', | |
apex_web_service.g_status_code, l_response, l_request); | |
raise e_api_call_failed; | |
end if; | |
l_response_json := json_object_t(l_response); | |
return l_response_json.get_number('totalTokens'); | |
end count_tokens; | |
/** | |
キャッシュするコンテンツのおおよそのトークン数を確認する。 | |
systemInstructionもキャッシュの対象になっているが、それはトークンの計算に含めない。 | |
*/ | |
function estimate_token_count( | |
p_text in clob | |
,p_selected_files in varchar2 | |
,p_model in varchar2 default C_MODEL_DEFAULT | |
,p_credential_static_id in varchar2 | |
) return number | |
as | |
l_mime_types apex_t_varchar2; | |
l_file_uris apex_t_varchar2; | |
l_total_tokens number; | |
l_contents json_array_t; | |
begin | |
select_files( | |
p_selected_files => p_selected_files | |
,p_mime_types => l_mime_types | |
,p_file_uris => l_file_uris | |
); | |
create_contents_object( | |
p_text => p_text | |
,p_mime_types => l_mime_types | |
,p_file_uris => l_file_uris | |
,p_contents => l_contents | |
); | |
l_total_tokens := count_tokens( | |
p_contents => l_contents | |
,p_model => p_model | |
,p_credential_static_id => p_credential_static_id | |
); | |
return l_total_tokens; | |
end estimate_token_count; | |
end "UTL_GOOGLE_GEMINI_CONTEXT_CACHING"; | |
/ |