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は発生していません。

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を作成し、ブレッドクラムのリージョンに配置しています。


ボタン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では、以下のコードを実行します。

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;

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


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

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


UTL_GOOGLE_GEMINI_CONTEXT_CACHING:パッケージ定義

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: パッケージ本体

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";
/