2024年1月15日月曜日

Google Geminiに動画について説明してもらう

Google GeminiのAPIを呼び出して、動画について説明してもらいます。説明してもらう動画ファイルはGoogle Cloud Storageにアップロードし、 そのURIをAPIリクエストのfileDatafileUriとして指定します。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

今回の作業で追加、変更した機能は以下になります。
  1. APIの呼び出しをGoogle AIからVertex AIに変更しています。
  2. Google Cloud Storageの操作画面を追加しています。操作画面の作成方法は、記事「Google Cloud StorageにCloud Storage JSON APIでアクセスする」にて紹介しています。
  3. Gemini APIの呼び出しにあたって、Google Cloud Storage上のファイルをリクエストに含めることができるページを作成しています。
  4. ベクトル埋め込み(embedding)の生成に、Vertex AIのモデルmultimodalembeddingを使うように変更しました。
Google Gemini APIのAPIエンドポイントは以下です。(モデルがGemini Proの場合)
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_IDG_REGIONを作成し、値を設定します。


最初にGoogle Cloud Storage上のファイルを選択する際に使用する、共有コンポーネントLOVを作成します。


作成済みのLOVが一覧されます。作成をクリックします。


LOVの作成最初からです。

へ進みます。


名前Google Cloud Storage Filesとします。タイプDynamicを選択します。

へ進みます。


データ・ソースとしてRESTデータ・ソースを選択します。RESTデータ・ソースGoogle Cloud Storage JSON APIを選択します。このデータ・ソースは、Google Cloud StorageのファイルをアクセスするページをAPEXアプリケーションに追加する際に作成しています。

へ進みます。


戻り列表示列ともにNAMEを選択します。

作成をクリックします。


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を呼び出すページは、ページ番号Imageのページをコピーして作成します。

ページの作成をクリックします。


コピーとしてのページの作成をクリックします。


次のコピーとしてのページを作成として、このアプリケーションのページを選択します。

へ進みます。


コピー元ページとして3. Imageを選択します。新規ページ番号10新規ページ名Movieとします。

ページにブレッドクラムを作るため、ブレッドクラムを選択します。親エントリなしエントリ名Movieとします。

へ進みます。


ナビゲーションのプリファレンスとして新規ナビゲーション・メニュー・エントリの作成を選びます。

新規ナビゲーション・メニュー・エントリMovie親ナビゲーション・メニュー・エントリとして- 親が選択されていません -を選択します。

へ進みます。


ラベルImageMovieに変更します。

コピーをクリックします。


ページがコピーされます。

コンテント・タイプを保持するページ・アイテムを作成します。

識別名前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コードも以下に入れ替えます。

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のコード:
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;
/