Google GeminiのAPIを呼び出して、動画について説明してもらいます。説明してもらう動画ファイルはGoogle Cloud Storageにアップロードし、 そのURIをAPIリクエストのfileDataにfileUriとして指定します。APIリクエストにファイルをそのまま埋め込む場合は、inlineDataとしてbase64でエンコーディングした文字列を指定できますが、APIリクエストの最大サイズに制限されます。
Gemini APIでGoogle Cloud Storage上のファイルを扱うには、Vertex AIのGemini APIを呼び出す必要があります。APIの認証はAPIキーではなく、サービス・アカウントによる認証を使います。
以下のGIF動画では、キリンの動画をGoogle Cloud Storageにアップロードし、動画の説明をお願いしています。
作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/google-vertex-gemini.zip
今回の作業で追加、変更した機能は以下になります。
- APIの呼び出しをGoogle AIからVertex AIに変更しています。
- Google Cloud Storageの操作画面を追加しています。操作画面の作成方法は、記事「Google Cloud StorageにCloud Storage JSON APIでアクセスする」にて紹介しています。
- Gemini APIの呼び出しにあたって、Google Cloud Storage上のファイルをリクエストに含めることができるページを作成しています。
- ベクトル埋め込み(embedding)の生成に、Vertex AIのモデルmultimodalembeddingを使うように変更しました。
https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent
Vertex AI Gemini APIのAPIエンドポイントは以下です。(同等のモデルの場合)
https://{REGION}-aiplatform.googleapis.com/v1/projects/{PROJECT_ID}/locations/{REGION}/publishers/google/models/{MODEL_ID}:streamGenerateContent
Vertex AIではgenerateContentではなくstreamGenerateContentを呼び出します。送信するJSONのリクエストの仕様には差異は無さそうですが、streamGenerateContentでは単一の応答の代わりに応答の配列が返されます。
Googleの以下のドキュメントでは、属性candidatesを含みJSONドキュメントが応答として記載されています。
実際には属性candidatesを含むJSONオブジェクトの配列が返されます。
[{
"candidates": [
{
"content": {
"role": "model",
"parts": [
{
"text": "* **成人式とは?**\n\n 成人式とは、毎年1月1"
}
]
},
"safetyRatings": [
{
"category": "HARM_CATEGORY_HARASSMENT",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_HATE_SPEECH",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"probability": "NEGLIGIBLE"
}
]
}
]
}
,
{
"candidates": [
{
"content": {
"role": "model",
"parts": [
{
"text": "5日に行われる日本の国民的行事で、20歳になった男女を祝"
}
]
},
"safetyRatings": [
{
"category": "HARM_CATEGORY_HARASSMENT",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_HATE_SPEECH",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"probability": "NEGLIGIBLE"
}
]
}
]
}
,
/* この繰り返しなので省略します。 */
{
"candidates": [
{
"content": {
"role": "model",
"parts": [
{
"text": "、日本の伝統的な衣装であり、成人式に振袖を着ることで、成人になったことを祝う意味があります。振袖は、未婚女性の礼装であり、成人式だけでなく、結婚式や卒業式などにも着られます。\n\n\n* **成人式の過ごし方**\n\n 成人式は、20歳になったことを祝うため、友人や家族と食事をしたり、旅行に出かけたりして過ごす人が"
}
]
},
"safetyRatings": [
{
"category": "HARM_CATEGORY_HARASSMENT",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_HATE_SPEECH",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"probability": "NEGLIGIBLE"
}
]
}
]
}
,
{
"candidates": [
{
"content": {
"role": "model",
"parts": [
{
"text": "多いです。また、成人式の前には、成人式の記念写真を撮影したり、成人式の準備をしたりして、成人式を祝う準備をします。"
}
]
},
"finishReason": "STOP",
"safetyRatings": [
{
"category": "HARM_CATEGORY_HARASSMENT",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_HATE_SPEECH",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"probability": "NEGLIGIBLE"
}
]
}
],
"usageMetadata": {
"promptTokenCount": 8,
"candidatesTokenCount": 373,
"totalTokenCount": 381
}
}
]
Googleから提供されているSDKを使ってGemini APIを呼び出していると、低レベルの動作を気にする必要はないのですが、PL/SQLではそうも行きません。GoogleのAPIのドキュメントには、レスポンスのフォーマットについて詳細が記載されていないため、今回の実装ではレスポンスの属性candidatesの配列の要素は1つだけ、属性partsの配列の要素も1つだけ、そして属性textも1つだけ含まれる、という前提でAPIのレスポンスを処理しています。
Gemini APIを呼び出すパッケージをUTL_GOOGLE_GEMINI_APIとして作成しています。これを少し変更してVertex AIのGemini APIを呼び出すようにしたパッケージUTL_VERTEX_AI_GEMINI_APIを作成しました。コードは記事の末尾に添付しています。
APIのエンドポイントをVertex AIに変更し、上記のレスポンスの扱いも変更しているため、同名のプロシージャやファンクションでも引数は異なっています。
以下より、動画のfileUriをAPIのリクエストに含めて、マルチモーダルの問い合わせを行なうページを作成します。
APIエンドポイントを決める際にプロジェクトIDとリージョンの指定が必要になります。そのため、アプリケーション定義の置換に置換文字列としてG_PROJECT_IDとG_REGIONを作成し、値を設定します。
最初にGoogle Cloud Storage上のファイルを選択する際に使用する、共有コンポーネントのLOVを作成します。
作成済みのLOVが一覧されます。作成をクリックします。
LOVの作成は最初からです。
次へ進みます。
次へ進みます。
データ・ソースとしてRESTデータ・ソースを選択します。RESTデータ・ソースはGoogle Cloud Storage JSON APIを選択します。このデータ・ソースは、Google Cloud StorageのファイルをアクセスするページをAPEXアプリケーションに追加する際に作成しています。
次へ進みます。
作成をクリックします。
LOVとしてGOOGLE CLOUD STORAGE FILESが作成されます。
編集するために、リンクをクリックします。
RESTソース・パラメータbucketの鉛筆アイコンをクリックし、bucketの値を指定します。
値タイプは静的、静的値として&G_BUCKET_NAME.を設定します。
変更の適用をクリックします。
追加表示列の列の選択をクリックします。
列NAMEに加えて列CONTENTTYPEも表示されるようにします。
列CONTENTTYPE(Varchar2)を選択し、更新をクリックします。
追加表示列としてCONTENTTYPEが追加されます。
列NAMEは最初から表示列となっていますが、列NAMEは戻り列として設定されているため、ヘッダーに値がありません。また表示可能などもNoとなっています。
列NAMEのヘッダーにNameを設定し、表示可能および検索可能をYesに変更します。
変更の適用をクリックします。
以上でLOVの作成は完了です。
Gemini APIを呼び出すページは、ページ番号3のImageのページをコピーして作成します。
ページの作成をクリックします。
コピーとしてのページの作成をクリックします。
次へ進みます。
コピー元ページとして3. Imageを選択します。新規ページ番号は10、新規ページ名はMovieとします。
ページにブレッドクラムを作るため、ブレッドクラムを選択します。親エントリなし、エントリ名はMovieとします。
次へ進みます。
ナビゲーションのプリファレンスとして新規ナビゲーション・メニュー・エントリの作成を選びます。
新規ナビゲーション・メニュー・エントリはMovie、親ナビゲーション・メニュー・エントリとして- 親が選択されていません -を選択します。
次へ進みます。
コピーをクリックします。
ページがコピーされます。
コンテント・タイプを保持するページ・アイテムを作成します。
識別の名前はP10_CONTENT_TYPE、タイプはテキスト・フィールド、ラベルはContent Typeとします。
デバッグのためにAPIのレスポンスをそのまま表示するページ・アイテムを作成します。
識別の名前はP10_RESPONSE_RAW、タイプはテキスト領域、ラベルはResponse Rawとします。
大きなデータも扱えるように、セッション・ステートのデータ型にCLOBを選択します。
ページ・アイテムP10_IMAGEを選択し、ローカルのコンピュータ上のファイルを選択するページ・アイテムから、Google Cloud Storage上のファイルを選択するページ・アイテムに変更します。
識別の名前をP10_MOVIEに変更します。タイプはポップアップLOVに変更します。ラベルはMovieに変更済みです。
追加出力としてCONTENTTYPE:P10_CONTENT_TYPEを設定します。APIリクエストのfileDataは複数(最大16個まで)にすることができますが、今回の実装では1つだけにしています。複数の値をオンにすると追加出力ができなくなるため、複数のファイルを選択するには列CONTENT_TYPEの値を取る追加の実装が必要になります。
LOVのタイプに共有コンポーネントを選択し、LOVとしてGOOGLE CLOUD STORAGE FILESを選択します。追加値の表示はオフ、NULL表示値として- ファイルを選択 -を設定します。
セッション・ステートのストレージとしてリクエストごと(メモリーのみ)を選択します。理由は不明ですが、セッションごと(永続)の場合エラーが発生しました。
左ペインでプロセス・ビューを開き、プロセス画像を含む呼び出しを選択します。
名前を動画を含む呼び出しに変更し、ソースのPL/SQLコードも以下に入れ替えます。
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_file_uri varchar2(200); | |
l_response clob; | |
begin | |
l_file_uri := 'gs://' || :G_BUCKET_NAME || '/' || :P10_MOVIE; | |
utl_vertex_ai_gemini_api.generate_content( | |
p_project_id => :G_PROJECT_ID | |
,p_region => :G_REGION | |
,p_text => :P10_TEXT | |
,p_file_uri => l_file_uri | |
,p_mimetype => :P10_CONTENT_TYPE | |
,p_response => l_response | |
,p_credential_static_id => :G_CREDENTIAL | |
); | |
:P10_RESPONSE := utl_vertex_ai_gemini_api.get_concat_text_in_all_candidates( | |
p_response => l_response | |
); | |
:P10_RESPONSE_RAW := l_response; | |
end; |
保存をクリックします。
以上でアプリケーションは完成です。アプリケーションを実行すると、記事の先頭のGIF動画のように動作します。
Googleのドキュメント「マルチモーダル プロンプト リクエストを送信する」を参照すると、色々と考慮しなければいけないことが書かれています。
https://cloud.google.com/vertex-ai/docs/generative-ai/multimodal/send-multimodal-prompts
Oracle APEXのアプリケーション作成の参考になれば幸いです。
完
パッケージUTL_VERTEX_AI_GEMINI_APIのコード:
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_vertex_ai_gemini_api | |
as | |
/* | |
* API Reference | |
* https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/gemini | |
*/ | |
/* threshold */ | |
C_THRESHOLD_BLOCK_NONE constant varchar2(30) := 'BLOCK_NONE'; | |
C_THRESHOLD_BLOCK_LOW_AND_ABOVE constant varchar2(30) := 'BLOCK_LOW_AND_ABOVE'; | |
/* BLOCK_MEDIUM_AND_ABOVE or BLOCK_MED_AND_ABOVE ? */ | |
C_THRESHOLD_BLOCK_MEDIUM_AND_ABOVE constant varchar2(30) := 'BLOCK_MEDIUM_AND_ABOVE'; | |
C_THRESHOLD_BLOCK_HIGH_AND_ABOVE constant varchar2(30) := 'BLOCK_LOW_AND_ABOVE'; | |
/* finishReason */ | |
C_FINISH_REASON_UNSPECIFIED constant varchar2(12) := 'UNSPECIFIED'; | |
C_FINISH_REASON_STOP constant varchar2(12) := 'STOP'; | |
C_FINISH_REASON_MAX_TOKENS constant varchar2(12) := 'MAX_TOKENS'; | |
C_FINISH_REASON_SAFETY constant varchar2(12) := 'SAFETY'; | |
C_FINISH_REASON_RECITATION constant varchar2(12) := 'RECITATION'; | |
C_FINISH_REASON_OTHER constant varchar2(12) := 'OTHER'; | |
/* probability */ | |
C_HARM_PROBABILITY_UNSPECIFIED constant varchar2(30) := 'HARM_PROBABILITY_UNSPECIFIED'; | |
C_HARM_PROBABILITY_NEGLIGIBLE constant varchar2(30) := 'NEGLIGIBLE'; | |
C_HARM_PROBABILITY_LOW constant varchar2(30) := 'LOW'; | |
C_HARM_PROBABILITY_MEDIUM constant varchar2(30) := 'MEDIUM'; | |
C_HARM_PROBABILITY_HIGH constant varchar2(30) := 'HIGH'; | |
/* API Endpoints gemini-pro or gemini-pro-vision */ | |
C_URL_GENERATE_CONTENT constant varchar2(400) := 'https://{REGION}-aiplatform.googleapis.com/v1/projects/{PROJECT_ID}/locations/{REGION}/publishers/google/models/{MODEL_ID}:streamGenerateContent'; | |
C_URL_COUNT_TOKENS constant varchar2(400) := 'https://{REGION}-aiplatform.googleapis.com/v1/projects/{PROJECT_ID}/locations/{REGION}/publishers/google/models/{MODEL_ID}:countTokens'; | |
C_URL_EMBED_CONTENT constant varchar2(400) := 'https://{REGION}-aiplatform.googleapis.com/v1/projects/{PROJECT_ID}/locations/{REGION}/publishers/google/models/{MODEL_ID}:predict'; | |
/** | |
* get first object within parts array which is also first object within candidates array. | |
*/ | |
function get_first_part( | |
p_candidates in json_array_t | |
,p_ignore in boolean default true | |
,p_role out varchar2 | |
) return json_object_t; | |
/** | |
* get text value from the first part object. | |
*/ | |
function get_first_text( | |
p_candidates in json_array_t | |
,p_ignore in boolean default true | |
,p_role out varchar2 | |
) return clob; | |
/** | |
* streamGenerateContent returns multiple condidates in json array. | |
*/ | |
function get_concat_text_in_all_candidates( | |
p_response in clob | |
) | |
return clob; | |
/** | |
* if part object is functionCall, call the function and return the response as clob. | |
*/ | |
function call_function( | |
p_part in json_object_t | |
) return clob; | |
/** | |
* Text-only input, single-turn, model is gemini-pro. | |
*/ | |
procedure generate_content( | |
p_project_id in varchar2 | |
,p_region in varchar2 | |
,p_text in clob | |
-- generationConfig | |
,p_temperature in number default 0.9 | |
,p_topK in number default 1 | |
,p_topP in number default 1 | |
,p_max_output_tokens in number default 2048 | |
,p_stop_sequences in clob default null | |
-- safetySettings | |
,p_harm_category_harassment in varchar2 default C_THRESHOLD_BLOCK_MEDIUM_AND_ABOVE | |
,p_harm_category_hate_speech in varchar2 default C_THRESHOLD_BLOCK_MEDIUM_AND_ABOVE | |
,p_harm_category_sexually_explicit in varchar2 default C_THRESHOLD_BLOCK_MEDIUM_AND_ABOVE | |
,p_harm_category_dangerous_content in varchar2 default C_THRESHOLD_BLOCK_MEDIUM_AND_ABOVE | |
,p_credential_static_id in varchar2 | |
,p_response out clob | |
); | |
/** | |
* Text-and-image input, single-turn, model is gemini-pro-vision. | |
* Currently, multi-turn is not recommended with Text-and-image. | |
*/ | |
procedure generate_content( | |
p_project_id in varchar2 | |
,p_region in varchar2 | |
,p_text in clob | |
,p_image in blob | |
,p_mimetype in varchar2 | |
-- generationConfig | |
,p_temperature in number default 0.4 | |
,p_topK in number default 32 | |
,p_topP in number default 1 | |
,p_max_output_tokens in number default 2048 | |
,p_stop_sequences in clob default null | |
-- safetySettings | |
,p_harm_category_harassment in varchar2 default C_THRESHOLD_BLOCK_MEDIUM_AND_ABOVE | |
,p_harm_category_hate_speech in varchar2 default C_THRESHOLD_BLOCK_MEDIUM_AND_ABOVE | |
,p_harm_category_sexually_explicit in varchar2 default C_THRESHOLD_BLOCK_MEDIUM_AND_ABOVE | |
,p_harm_category_dangerous_content in varchar2 default C_THRESHOLD_BLOCK_MEDIUM_AND_ABOVE | |
,p_credential_static_id in varchar2 | |
,p_response out clob | |
); | |
/** | |
* Text-and-image or movie, fileURI on Object Storage instead of blob. | |
* single-turn, model is gemini-pro-vision | |
*/ | |
procedure generate_content( | |
p_project_id in varchar2 | |
,p_region in varchar2 | |
,p_text in clob | |
,p_file_uri in varchar2 | |
,p_mimetype in varchar2 | |
-- generationConfig | |
,p_temperature in number default 0.4 | |
,p_topK in number default 32 | |
,p_topP in number default 1 | |
,p_max_output_tokens in number default 2048 | |
,p_stop_sequences in clob default null | |
-- safetySettings | |
,p_harm_category_harassment in varchar2 default C_THRESHOLD_BLOCK_MEDIUM_AND_ABOVE | |
,p_harm_category_hate_speech in varchar2 default C_THRESHOLD_BLOCK_MEDIUM_AND_ABOVE | |
,p_harm_category_sexually_explicit in varchar2 default C_THRESHOLD_BLOCK_MEDIUM_AND_ABOVE | |
,p_harm_category_dangerous_content in varchar2 default C_THRESHOLD_BLOCK_MEDIUM_AND_ABOVE | |
,p_credential_static_id in varchar2 | |
,p_response out clob | |
); | |
/** | |
* Text-only input, multi-turn, model is gemini-pro. | |
*/ | |
procedure generate_content( | |
p_project_id in varchar2 | |
,p_region in varchar2 | |
,p_contents in clob | |
,p_tools in clob default null | |
-- generationConfig | |
,p_temperature in number default 0.9 | |
,p_topK in number default 1 | |
,p_topP in number default 1 | |
,p_max_output_tokens in number default 2048 | |
,p_stop_sequences in clob default null | |
-- safetySettings | |
,p_harm_category_harassment in varchar2 default C_THRESHOLD_BLOCK_MEDIUM_AND_ABOVE | |
,p_harm_category_hate_speech in varchar2 default C_THRESHOLD_BLOCK_MEDIUM_AND_ABOVE | |
,p_harm_category_sexually_explicit in varchar2 default C_THRESHOLD_BLOCK_MEDIUM_AND_ABOVE | |
,p_harm_category_dangerous_content in varchar2 default C_THRESHOLD_BLOCK_MEDIUM_AND_ABOVE | |
,p_credential_static_id in varchar2 | |
,p_response out clob | |
); | |
/** | |
* Cout tokens. | |
*/ | |
function count_tokens( | |
p_project_id in varchar2 | |
,p_region in varchar2 | |
/* specifiy p_text OR p_contents */ | |
,p_text in clob default null | |
,p_parts in clob default null /* for image */ | |
,p_credential_static_id in varchar2 | |
) return number; | |
/** | |
* Embedding. | |
*/ | |
procedure embed_content( | |
p_project_id in varchar2 | |
,p_region in varchar2 | |
,p_model_id in varchar2 default 'multimodalembedding@001' | |
/* p_text or p_image is required */ | |
,p_text in clob default null | |
,p_image in blob default null | |
,p_embedding_text out clob | |
,p_dimension_text out number | |
,p_embedding_image out clob | |
,p_dimension_image out number | |
,p_credential_static_id in varchar2 | |
); | |
end utl_vertex_ai_gemini_api; | |
/ | |
create or replace package body utl_vertex_ai_gemini_api | |
as | |
/** | |
* complement vertex ai gemini api endpoint with region and project_id. | |
*/ | |
function completeEndpointURL( | |
p_endpoint_url in varchar2 | |
,p_project_id in varchar2 | |
,p_region in varchar2 | |
,p_model_id in varchar2 | |
) return varchar2 | |
as | |
l_endpoint_url varchar2(400); | |
begin | |
l_endpoint_url := replace(p_endpoint_url, '{REGION}', p_region); | |
l_endpoint_url := replace(l_endpoint_url, '{PROJECT_ID}', p_project_id); | |
l_endpoint_url := replace(l_endpoint_url, '{MODEL_ID}', p_model_id); | |
return l_endpoint_url; | |
end completeEndpointURL; | |
/** | |
* private function for creating generationConfig object. | |
*/ | |
function generate_generation_config( | |
p_temperature in number | |
,p_topK in number | |
,p_topP in number | |
,p_max_output_tokens in number | |
,p_stop_sequences in clob | |
) return json_object_t | |
as | |
l_generation_config json_object_t; | |
begin | |
l_generation_config := json_object_t( | |
json_object( | |
'temperature' value p_temperature | |
,'topK' value p_topK | |
,'topP' value p_topP | |
,'maxOutputTokens' value p_max_output_tokens | |
,'stopSequences' value p_stop_sequences | |
) | |
); | |
return l_generation_config; | |
end generate_generation_config; | |
/** | |
* privete function for creating safetySettings array. | |
*/ | |
function generate_sefety_settings( | |
p_harm_category_harassment in varchar2 | |
,p_harm_category_hate_speech in varchar2 | |
,p_harm_category_sexually_explicit in varchar2 | |
,p_harm_category_dangerous_content in varchar2 | |
) return json_array_t | |
as | |
l_safety_settings json_array_t := json_array_t(); | |
begin | |
l_safety_settings.append(json_object_t( | |
json_object( | |
'category' value 'HARM_CATEGORY_HARASSMENT' | |
,'threshold' value p_harm_category_harassment | |
) | |
)); | |
l_safety_settings.append(json_object_t( | |
json_object( | |
'category' value 'HARM_CATEGORY_HATE_SPEECH' | |
,'threshold' value p_harm_category_hate_speech | |
) | |
)); | |
l_safety_settings.append(json_object_t( | |
json_object( | |
'category' value 'HARM_CATEGORY_SEXUALLY_EXPLICIT' | |
,'threshold' value p_harm_category_sexually_explicit | |
) | |
)); | |
l_safety_settings.append(json_object_t( | |
json_object( | |
'category' value 'HARM_CATEGORY_DANGEROUS_CONTENT' | |
,'threshold' value p_harm_category_dangerous_content | |
) | |
)); | |
return l_safety_settings; | |
end generate_sefety_settings; | |
/** | |
* get first part object from the response. | |
*/ | |
function get_first_part( | |
p_candidates in json_array_t | |
,p_ignore in boolean | |
,p_role out varchar2 | |
) return json_object_t | |
as | |
l_candidate json_object_t; | |
l_content json_object_t; | |
l_parts json_array_t; | |
l_part json_object_t; | |
l_finish_reason varchar2(20); | |
e_too_many_candidates exception; | |
e_too_many_parts exception; | |
begin | |
if p_candidates.get_size() > 1 then | |
/* never happen because candidateCount is always 1 at this moment. */ | |
if not p_ignore then | |
raise e_too_many_candidates; | |
end if; | |
end if; | |
l_candidate := treat(p_candidates.get(0) as json_object_t); | |
/* assumes finishReason is always "STOP" */ | |
l_finish_reason := l_candidate.get_string('finishReason'); | |
if l_finish_reason <> C_FINISH_REASON_STOP then | |
/* not sure how to handle, just log */ | |
apex_debug.info('finishReason = %', l_finish_reason); | |
if l_finish_reason = C_FINISH_REASON_SAFETY then | |
apex_debug.info('safetyRatings: %s', l_candidate.get_object('safetyRatings').to_clob()); | |
end if; | |
end if; | |
/* | |
* candidate (object in candidates array) contains finishReason, index, safetyRatings | |
* in addition to content. | |
*/ | |
l_content := l_candidate.get_object('content'); | |
p_role := l_content.get_string('role'); | |
l_parts := l_content.get_array('parts'); | |
if l_parts.get_size() > 1 then | |
if not p_ignore then | |
raise e_too_many_parts; | |
end if; | |
end if; | |
l_part := treat(l_parts.get(0) as json_object_t); | |
return l_part; | |
end get_first_part; | |
/** | |
* get text value and role from part object in the response. | |
*/ | |
function get_first_text( | |
p_candidates in json_array_t | |
,p_ignore in boolean | |
,p_role out varchar2 | |
) return clob | |
as | |
l_part json_object_t; | |
begin | |
l_part := get_first_part( | |
p_candidates => p_candidates | |
,p_ignore => p_ignore | |
,p_role => p_role | |
); | |
return l_part.get_string('text'); | |
end get_first_text; | |
/** | |
* streamGenerateContent returns multiple condidates in json array. | |
*/ | |
function get_concat_text_in_all_candidates( | |
p_response in clob | |
) | |
return clob | |
as | |
l_response_array json_array_t; | |
l_text clob := ''; | |
l_object json_object_t; | |
l_candidates json_array_t; | |
l_role varchar2(10); | |
begin | |
l_response_array := json_array_t(p_response); | |
for i in 1..l_response_array.get_size() | |
loop | |
l_object := treat(l_response_array.get(i-1) as json_object_t); | |
l_candidates := l_object.get_array('candidates'); | |
l_text := l_text || get_first_text( | |
p_candidates => l_candidates | |
,p_role => l_role); | |
end loop; | |
return l_text; | |
end get_concat_text_in_all_candidates; | |
/** | |
* call function that is replied in functionCall. | |
* all functions passed in tools must be created as stored procedure. | |
* Ref: | |
* https://cloud.google.com/vertex-ai/docs/generative-ai/multimodal/function-calling | |
*/ | |
function call_function( | |
p_part in json_object_t | |
) return clob | |
as | |
l_function json_object_t; | |
l_function_name varchar2(200); | |
l_function_args clob; | |
l_dynamic_sql varchar2(4000); | |
l_function_out clob; | |
l_response json_object_t := json_object_t(); | |
l_function_response_json json_object_t := json_object_t(); | |
l_function_response clob; | |
begin | |
l_function := p_part.get_object('functionCall'); | |
if l_function is null then | |
/* do nothing */ | |
return null; | |
end if; | |
l_function_name := l_function.get_string('name'); | |
l_function_args := l_function.get_object('args').to_clob(); | |
/* call stored procedure defined in the database dynamically. */ | |
l_dynamic_sql := 'begin :a := ' || l_function_name || '(:b); end;'; | |
execute immediate l_dynamic_sql using in out l_function_out, l_function_args; | |
l_function_response_json.put('name', l_function_name); | |
l_response.put('name', l_function_name); | |
l_response.put('content', json_object_t(l_function_out)); | |
l_function_response_json.put('response', l_response); | |
l_function_response := l_function_response_json.to_clob(); | |
return l_function_response; | |
end call_function; | |
/** | |
* Private procedure to send request to Gemini. | |
*/ | |
procedure generate_content( | |
p_project_id in varchar2 | |
,p_region in varchar2 | |
,p_model_id in varchar2 | |
,p_contents in clob | |
,p_tools in clob default null | |
-- generationConfig | |
,p_temperature in number | |
,p_topK in number | |
,p_topP in number | |
,p_max_output_tokens in number | |
,p_stop_sequences in clob | |
-- safetySettings | |
,p_harm_category_harassment in varchar2 | |
,p_harm_category_hate_speech in varchar2 | |
,p_harm_category_sexually_explicit in varchar2 | |
,p_harm_category_dangerous_content in varchar2 | |
,p_credential_static_id in varchar2 | |
,p_response out clob | |
) | |
as | |
l_endpoint_url varchar2(400); | |
l_request json_object_t := json_object_t(); | |
l_safety_settings json_array_t := json_array_t(); | |
l_request_clob clob; | |
l_response_json json_object_t; | |
l_response clob; | |
e_api_call_failed exception; | |
begin | |
-- Endpoint | |
l_endpoint_url := completeEndpointURL( | |
p_endpoint_url => C_URL_GENERATE_CONTENT | |
,p_project_id => p_project_id | |
,p_region => p_region | |
,p_model_id => p_model_id | |
); | |
-- contents | |
l_request.put('contents', json_array_t(p_contents)); | |
if p_tools is not null then | |
l_request.put('tools', json_array_t(p_tools)); | |
end if; | |
-- generationConfig | |
l_request.put('generationConfig', | |
generate_generation_config( | |
p_temperature => p_temperature | |
,p_topK => p_topK | |
,p_topP => p_topP | |
,p_max_output_tokens => p_max_output_tokens | |
,p_stop_sequences => p_stop_sequences | |
) | |
); | |
-- safetySettings | |
l_request.put('safetySettings', | |
generate_sefety_settings( | |
p_harm_category_harassment => p_harm_category_harassment | |
,p_harm_category_hate_speech => p_harm_category_hate_speech | |
,p_harm_category_sexually_explicit => p_harm_category_sexually_explicit | |
,p_harm_category_dangerous_content => p_harm_category_dangerous_content | |
) | |
); | |
l_request_clob := l_request.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_endpoint_url | |
,p_http_method => 'POST' | |
,p_body => l_request_clob | |
,p_credential_static_id => p_credential_static_id | |
); | |
if apex_web_service.g_status_code <> 200 then | |
raise e_api_call_failed; | |
end if; | |
p_response := l_response; | |
end generate_content; | |
/** | |
* Text-only input, single-trun, gemini-pro. | |
*/ | |
procedure generate_content( | |
p_project_id in varchar2 | |
,p_region in varchar2 | |
,p_text in clob | |
-- generationConfig | |
,p_temperature in number | |
,p_topK in number | |
,p_topP in number | |
,p_max_output_tokens in number | |
,p_stop_sequences in clob | |
-- safetySettings | |
,p_harm_category_harassment in varchar2 | |
,p_harm_category_hate_speech in varchar2 | |
,p_harm_category_sexually_explicit in varchar2 | |
,p_harm_category_dangerous_content in varchar2 | |
,p_credential_static_id in varchar2 | |
,p_response out clob | |
) | |
as | |
l_contents json_array_t := json_array_t(); | |
l_content json_object_t := json_object_t(); | |
l_parts json_array_t := json_array_t(); | |
l_part json_object_t := json_object_t(); | |
l_contents_clob clob; | |
l_response clob; | |
begin | |
l_part.put('text', p_text); | |
l_parts.append(l_part); | |
l_content.put('role', 'user'); | |
l_content.put('parts', l_parts); | |
l_contents.append(l_content); | |
l_contents_clob := l_contents.to_clob(); | |
generate_content( | |
p_project_id => p_project_id | |
,p_region => p_region | |
,p_model_id => 'gemini-pro' | |
,p_contents => l_contents_clob | |
,p_temperature => p_temperature | |
,p_topK => p_topK | |
,p_topP => p_topP | |
,p_max_output_tokens => p_max_output_tokens | |
,p_stop_sequences => p_stop_sequences | |
,p_harm_category_harassment => p_harm_category_harassment | |
,p_harm_category_hate_speech => p_harm_category_hate_speech | |
,p_harm_category_sexually_explicit => p_harm_category_sexually_explicit | |
,p_harm_category_dangerous_content => p_harm_category_dangerous_content | |
,p_credential_static_id => p_credential_static_id | |
,p_response => p_response | |
); | |
end generate_content; | |
/** | |
* Text-and-image, single-turn, gemini-pro-vision. | |
*/ | |
procedure generate_content( | |
p_project_id in varchar2 | |
,p_region in varchar2 | |
,p_text in clob | |
,p_image in blob | |
,p_mimetype in varchar2 | |
-- generationConfig | |
,p_temperature in number | |
,p_topK in number | |
,p_topP in number | |
,p_max_output_tokens in number | |
,p_stop_sequences in clob | |
-- safetySettings | |
,p_harm_category_harassment in varchar2 | |
,p_harm_category_hate_speech in varchar2 | |
,p_harm_category_sexually_explicit in varchar2 | |
,p_harm_category_dangerous_content in varchar2 | |
,p_credential_static_id in varchar2 | |
,p_response out clob | |
) | |
as | |
l_contents json_array_t := json_array_t(); | |
l_content json_object_t := json_object_t(); | |
l_parts json_array_t := json_array_t(); | |
l_part json_object_t; | |
l_image_clob clob; | |
l_inline_data json_object_t; | |
l_contents_clob clob; | |
begin | |
l_part := json_object_t(); | |
l_part.put('text', p_text); | |
l_parts.append(l_part); | |
if p_image is not null then | |
l_part := json_object_t(); | |
l_inline_data := json_object_t(); | |
l_inline_data.put('mimeType', p_mimetype); | |
l_image_clob := apex_web_service.blob2clobbase64(p_image, 'N','N'); | |
l_inline_data.put('data', l_image_clob); | |
l_part.put('inlineData', l_inline_data); | |
l_parts.append(l_part); | |
end if; | |
l_content.put('role', 'user'); | |
l_content.put('parts', l_parts); | |
l_contents.append(l_content); | |
l_contents_clob := l_contents.to_clob(); | |
generate_content( | |
p_project_id => p_project_id | |
,p_region => p_region | |
,p_model_id => 'gemini-pro-vision' | |
,p_contents => l_contents_clob | |
,p_temperature => p_temperature | |
,p_topK => p_topK | |
,p_topP => p_topP | |
,p_max_output_tokens => p_max_output_tokens | |
,p_stop_sequences => p_stop_sequences | |
,p_harm_category_harassment => p_harm_category_harassment | |
,p_harm_category_hate_speech => p_harm_category_hate_speech | |
,p_harm_category_sexually_explicit => p_harm_category_sexually_explicit | |
,p_harm_category_dangerous_content => p_harm_category_dangerous_content | |
,p_credential_static_id => p_credential_static_id | |
,p_response => p_response | |
); | |
end generate_content; | |
/** | |
* Text-and-image or movie, single-turn, gemini-pro-vision. | |
*/ | |
procedure generate_content( | |
p_project_id in varchar2 | |
,p_region in varchar2 | |
,p_text in clob | |
,p_file_uri in varchar2 | |
,p_mimetype in varchar2 | |
-- generationConfig | |
,p_temperature in number | |
,p_topK in number | |
,p_topP in number | |
,p_max_output_tokens in number | |
,p_stop_sequences in clob | |
-- safetySettings | |
,p_harm_category_harassment in varchar2 | |
,p_harm_category_hate_speech in varchar2 | |
,p_harm_category_sexually_explicit in varchar2 | |
,p_harm_category_dangerous_content in varchar2 | |
,p_credential_static_id in varchar2 | |
,p_response out clob | |
) | |
as | |
l_contents json_array_t := json_array_t(); | |
l_content json_object_t := json_object_t(); | |
l_parts json_array_t := json_array_t(); | |
l_part json_object_t; | |
l_image_clob clob; | |
l_file_data json_object_t; | |
l_contents_clob clob; | |
begin | |
l_part := json_object_t(); | |
l_part.put('text', p_text); | |
l_parts.append(l_part); | |
if p_file_uri is not null then | |
l_part := json_object_t(); | |
l_file_data := json_object_t(); | |
l_file_data.put('mimeType', p_mimetype); | |
l_file_data.put('fileUri', p_file_uri); | |
l_part.put('fileData', l_file_data); | |
l_parts.append(l_part); | |
end if; | |
l_content.put('role','user'); | |
l_content.put('parts', l_parts); | |
l_contents.append(l_content); | |
l_contents_clob := l_contents.to_clob(); | |
apex_debug.info(l_contents_clob); | |
generate_content( | |
p_project_id => p_project_id | |
,p_region => p_region | |
,p_model_id => 'gemini-pro-vision' | |
,p_contents => l_contents_clob | |
,p_temperature => p_temperature | |
,p_topK => p_topK | |
,p_topP => p_topP | |
,p_max_output_tokens => p_max_output_tokens | |
,p_stop_sequences => p_stop_sequences | |
,p_harm_category_harassment => p_harm_category_harassment | |
,p_harm_category_hate_speech => p_harm_category_hate_speech | |
,p_harm_category_sexually_explicit => p_harm_category_sexually_explicit | |
,p_harm_category_dangerous_content => p_harm_category_dangerous_content | |
,p_credential_static_id => p_credential_static_id | |
,p_response => p_response | |
); | |
end generate_content; | |
/** | |
* Text-only-input, multi-turn, gemini-pro. | |
*/ | |
procedure generate_content( | |
p_project_id in varchar2 | |
,p_region in varchar2 | |
,p_contents in clob | |
,p_tools in clob | |
-- generationConfig | |
,p_temperature in number | |
,p_topK in number | |
,p_topP in number | |
,p_max_output_tokens in number | |
,p_stop_sequences in clob | |
-- safetySettings | |
,p_harm_category_harassment in varchar2 | |
,p_harm_category_hate_speech in varchar2 | |
,p_harm_category_sexually_explicit in varchar2 | |
,p_harm_category_dangerous_content in varchar2 | |
,p_credential_static_id in varchar2 | |
,p_response out clob | |
) | |
as | |
l_contents json_array_t := json_array_t(); | |
l_contents_clob clob; | |
l_response clob; | |
begin | |
generate_content( | |
p_project_id => p_project_id | |
,p_region => p_region | |
,p_model_id => 'gemini-pro' | |
,p_contents => p_contents | |
,p_tools => p_tools | |
,p_temperature => p_temperature | |
,p_topK => p_topK | |
,p_topP => p_topP | |
,p_max_output_tokens => p_max_output_tokens | |
,p_stop_sequences => p_stop_sequences | |
,p_harm_category_harassment => p_harm_category_harassment | |
,p_harm_category_hate_speech => p_harm_category_hate_speech | |
,p_harm_category_sexually_explicit => p_harm_category_sexually_explicit | |
,p_harm_category_dangerous_content => p_harm_category_dangerous_content | |
,p_credential_static_id => p_credential_static_id | |
,p_response => p_response | |
); | |
end generate_content; | |
/** | |
* count tokens | |
*/ | |
function count_tokens( | |
p_project_id in varchar2 | |
,p_region in varchar2 | |
,p_text in clob default null | |
,p_parts in clob default null | |
,p_credential_static_id in varchar2 | |
) return number | |
as | |
l_endpoint_url varchar2(400); | |
l_request json_object_t := json_object_t(); | |
l_request_clob clob; | |
l_contents json_array_t := json_array_t(); | |
l_content json_object_t := json_object_t(); | |
l_parts json_array_t := json_array_t(); | |
l_part json_object_t := json_object_t(); | |
l_response clob; | |
l_response_json json_object_t; | |
l_total_tokens number; | |
e_no_arguments exception; | |
e_api_call_failed exception; | |
begin | |
if p_text is not null then | |
l_endpoint_url := completeEndpointURL( | |
p_endpoint_url => C_URL_COUNT_TOKENS | |
,p_project_id => p_project_id | |
,p_region => p_region | |
,p_model_id => 'gemini-pro' | |
); | |
l_part.put('text', p_text); | |
l_parts.append(l_part); | |
else | |
if p_parts is null then | |
raise e_no_arguments; | |
end if; | |
l_endpoint_url := completeEndpointURL( | |
p_endpoint_url => C_URL_COUNT_TOKENS | |
,p_project_id => p_project_id | |
,p_region => p_region | |
,p_model_id => 'gemini-pro-vision' | |
); | |
l_parts := json_array_t(p_parts); | |
end if; | |
l_content.put('parts', l_parts); | |
l_contents.append(l_content); | |
l_request.put('contents', l_contents); | |
l_request_clob := l_request.to_clob(); | |
-- apex_debug.info(l_request_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_endpoint_url | |
,p_http_method => 'POST' | |
,p_body => l_request_clob | |
,p_credential_static_id => p_credential_static_id | |
); | |
if apex_web_service.g_status_code <> 200 then | |
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; | |
/** | |
* Embedding | |
* multimodalembedding@001 supports both text and image. | |
*/ | |
procedure embed_content( | |
p_project_id in varchar2 | |
,p_region in varchar2 | |
,p_model_id in varchar2 default 'multimodalembedding@001' | |
,p_text in clob | |
,p_image in blob | |
,p_embedding_text out clob | |
,p_dimension_text out number | |
,p_embedding_image out clob | |
,p_dimension_image out number | |
,p_credential_static_id in varchar2 | |
) | |
as | |
l_endpoint_url varchar2(400); | |
l_request json_object_t := json_object_t(); | |
l_request_clob clob; | |
l_instances json_array_t := json_array_t(); | |
l_instance json_object_t; | |
l_image json_object_t; | |
l_response clob; | |
l_response_json json_object_t; | |
l_predictions json_array_t; | |
l_prediction json_object_t; | |
l_embedding_text json_array_t; | |
l_embedding_image json_array_t; | |
e_no_arguments exception; | |
e_api_call_failed exception; | |
begin | |
if p_text is null and p_image is null then | |
raise e_no_arguments; | |
end if; | |
l_endpoint_url := completeEndpointURL( | |
p_endpoint_url => C_URL_EMBED_CONTENT | |
,p_project_id => p_project_id | |
,p_region => p_region | |
,p_model_id => p_model_id | |
); | |
l_instance := json_object_t(); | |
if p_text is not null then | |
l_instance.put('text', p_text); | |
end if; | |
if p_image is not null then | |
l_image := json_object_t(); | |
l_image.put('bytesBase64Encoded', apex_web_service.blob2clobbase64(p_image, 'N','N')); | |
l_instance.put('image', l_image); | |
end if; | |
l_instances.append(l_instance); | |
l_request.put('instances', l_instances); | |
l_request_clob := l_request.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_endpoint_url | |
,p_http_method => 'POST' | |
,p_body => l_request_clob | |
,p_credential_static_id => p_credential_static_id | |
); | |
if apex_web_service.g_status_code <> 200 then | |
raise e_api_call_failed; | |
end if; | |
l_response_json := json_object_t(l_response); | |
l_predictions := l_response_json.get_array('predictions'); | |
l_prediction := treat(l_predictions.get(0) as json_object_t); | |
l_embedding_text := l_prediction.get_array('textEmbedding'); | |
if l_embedding_text is not null then | |
p_embedding_text := l_embedding_text.to_clob(); | |
p_dimension_text := l_embedding_text.get_size(); | |
end if; | |
l_embedding_image := l_prediction.get_array('imageEmbedding'); | |
if l_embedding_image is not null then | |
p_embedding_image := l_embedding_image.to_clob(); | |
p_dimension_image := l_embedding_image.get_size(); | |
end if; | |
end embed_content; | |
end utl_vertex_ai_gemini_api; | |
/ |