2023年12月24日日曜日

Google Geminiのマルチターンと関数呼び出しを実装する

Google Gemniを呼び出すアプリケーションにマルチターン(いわゆるチャット)と関数呼び出しを行うページを追加します。どちらの呼び出し方も、OpenAIのChat Completions APIに非常に近いため、以下の記事の実装を援用します。

OpenAIのChatGPTのAPIを呼び出すAPEXアプリを作る

作成したアプリケーションは以下のように動作します。

関数呼び出しが行われた場合(Chat HistoryのタイプでfunctionResponseが現れた場合)関数呼び出しの戻り値をGeminiに渡すため、Textを空白にしてRunをクリックする必要があります。


最初に関数呼び出しの、呼び出し対象となるファンクションを登録する表を作成します。

クイックSQLの以下のモデルより、表GEMINI_FUNCTIONSを作成します。

# prefix: gemini
functions
function_name vc200 /nn
function_set_name vc20 /nn
description vc4000 /nn
parameters json /nn
この表はOpenAIでの作業の際に作成した表OPENAI_FUNCTIONSとOPENAI_FUNCTION_SETを(不具合の修正も合わせて)簡素にしています。この表にサンプルとして設定するファンクションとして、OpenAIのときと同じget_current_weatherを使用します。


表を作成した後、この表にデータを投入するためにフォーム付き対話モード・レポートのページを作成します。

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


対話モード・レポートを選択します。


対話モード・レポートのページのページ名はFunctionsとします。フォーム・ページを含めるチェックします。フォーム・ページ名Function Detailとします。

データ・ソース表/ビューの名前GEMINI_FUNCTIONSを指定します。

へ進み、主キー列1ID (Number)を選択しページの作成をクリックすると、表GEMINI_FUNCTIONS対話モード・レポートフォームのページが作成されます。


ファンクションGET_CURRENT_WEATHERも作成しておきます。元記事に説明がありますが、このファンクションは引数である都市名から座標を得るために、アマノ技研さんより提供されている「世界の百万都市の位置データ Location Data of Megacities」のCSVファイルを表AMANO_CITY_LOCATIONSにロードしています。

create or replace function get_current_weather(
p_args in clob
)
return clob
as
l_args json_object_t;
l_location varchar2(400);
l_location_tokens apex_t_varchar2;
l_lat number;
l_lon number;
l_url varchar2(4000);
l_response clob;
l_response_json json_object_t;
l_weathercode number;
l_weather varchar2(4000);
begin
l_args := json_object_t(p_args);
l_location := l_args.get_string('location');
/* get first part (city name) from the location */
l_location_tokens := apex_string.split(l_location, '[ ,]');
l_location := l_location_tokens(1);
/*
* 都市名から緯度経度の情報を取り出す。
* アマノ技研さんより提供されている「世界の百万都市の位置データ Location Data of Megacities」の
* CSVファイルを表AMANO_CITY_LOCATIONSにロードしています。
* 参照: https://amano-tec.com/data/megacities.html
*/
select lat, lon into l_lat, l_lon
from amano_city_locations
where upper(capital_en) = upper(l_location) or capital_jp = l_location;
/*
* Open-Meteo.comのAPIを呼び出し、指定した座標の現在の天気を取得しています。
* 参照: https://open-meteo.com
*/
l_url := 'https://api.open-meteo.com/v1/forecast?latitude=' || l_lat || '&longitude=' || l_lon || '&current=weathercode';
l_response := apex_web_service.make_rest_request(
p_url => l_url
,p_http_method => 'GET'
);
if apex_web_service.g_status_code <> 200 then
return '{ "error": "open-meteo.com api call failed, response = ' || l_response || '" }';
end if;
l_response_json := json_object_t(l_response);
/*
* Weather Code (WMO 4501)のコードの説明は以下の気象関係コード表のWMO 4501の部分を
* 表WMO4501として読み込んでいます。
* 参照: https://www.jodc.go.jp/data_format/weather-code_j.html
*/
l_weathercode := l_response_json.get_object('current').get_number('weathercode');
select description into l_weather from wmo4501 where code = l_weathercode;
return '{ "weather" : "' || l_weather || '" }';
exception
when no_data_found then
return '{ "error": "no data found" }';
end get_current_weather;
/
アプリケーションを実行しFunctionsのページを開き、作成をクリックします。


ファンクションGET_CURRENT_WEATHERを登録します。

Function nameとしてget_current_weatherFunction Set NameとしてWeatherDescriptionとしてGet the current weather in a given locationParametersには以下のJSONドキュメントを記述します。
{
  "type": "object",
  "properties": {
    "location": {
      "type": "string",
      "description": "The city name"
    },
    "unit": {
      "type": "string",
      "enum": [
        "celsius",
        "fahrenheit"
      ]
    }
  },
  "required": [
    "location"
  ]
}
これらはOpenAIでFunction Callingを行う際に設定した内容とほぼ同じです。双方とも関数呼び出しの引数の定義は、OpenAPI 3.0に準拠しています。

作成をクリックします。


ファンクションget_current_weatherが設定されます。


APEXアプリケーションへマルチターンの処理を行うページを追加します。

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


空白のページを選択します。


ページ番号は、ページの名前Chatとします。

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


空白のページが作成されます。

会話を初期化するボタンINIT_CONVERSATIONを作成します。ラベルはInitialize Conversation動作アクションはデフォルトのページの送信です。テンプレート・オプションWidthStretchに変更しています。


プロセス・ビューを開き、このボタンINIT_CONVERSATIONがクリックされたときに実行されるプロセスを作成します。

作成したプロセスの識別名前Initialize Conversationとします。タイプコードを実行です。ソースPL/SQLコードに以下を記述します。チャット履歴を保持するAPEXコレクションGEMINIを初期化しています。
apex_collection.create_or_truncate_collection(
    p_collection_name => 'GEMINI'
);
サーバー側の条件ボタン押下時INIT_CONVERSATIONを指定します。


レンダリング・ビューに戻ります。

テキストを入力するページ・アイテムP6_TEXTを作成します。タイプテキスト領域ラベルTextとします。

セッション・ステートデータ型CLOBを選択し、続けて同じテキストを送信してしまわないように、ストレージとしてリクエストごと(メモリーのみ)を選択します。


ファンクション・セットを選択するページ・アイテムP6_FUNCTION_SETを作成します。

タイプ選択リストラベルFunction Setとします。LOVタイプSQL問合せを選択し、SQL問合せとして以下を記述します。
select function_set_name d, function_set_name r from gemini_functions group by function_set_name order by 1 asc
追加値の表示オフNULL値の表示オンとし、NULL表示値として- ファンクション・セットを選択 -と記述します。

セッション・ステートストレージセッションごと(永続)を選択します。


会話を実行するボタンRUNを作成します。ラベルはRun動作アクションはデフォルトのページの送信です。外観ホットオンにします。テンプレート・オプションWidthStretchに変更しています。


プロセス・ビューを開き、このボタンRUNがクリックされたときに実行されるプロセスを作成します。

作成したプロセスの識別名前Run Conversationとします。タイプコードを実行です。ソースPL/SQLコードに以下を記述します。

declare
l_user_text clob := :P6_TEXT;
l_functioin_set_name varchar2(40) := :P6_FUNCTION_SET;
--
l_contents json_array_t := json_array_t();
l_contents_clob clob;
l_content json_object_t;
l_parts json_array_t;
l_part json_object_t;
-- function calling
l_functions json_array_t;
l_function json_object_t;
l_parameters json_object_t;
l_tools json_array_t;
l_tools_clob clob;
l_tool json_object_t;
-- response
l_candidates json_array_t;
l_prompt_feedback json_object_t;
l_response clob;
l_reply clob;
l_role varchar2(8);
begin
/*
  * add user input first.
*/
if l_user_text is not null then
apex_collection.add_member(
p_collection_name => 'GEMINI'
,p_c001 => 'user'
,p_c002 => 'text'
,p_clob001 => l_user_text
);
end if;
/*
  * reconstruct chat history from apex collection.
*/
for r in (select c001, c002, clob001 from apex_collections where collection_name = 'GEMINI' order by seq_id)
loop
l_content := json_object_t();
l_content.put('role',r.c001);
l_part := json_object_t();
case r.c002
when 'text' then
l_part.put('text', r.clob001);
else
l_part.put(r.c002, json_object_t(r.clob001));
end case;
l_parts := json_array_t();
l_parts.append(l_part);
l_content.put('parts', l_parts);
l_contents.append(l_content);
end loop;
l_contents_clob := l_contents.to_clob();
-- prepare tools
if l_functioin_set_name is not null then
l_functions := json_array_t();
for r in (
select * from gemini_functions where function_set_name = l_functioin_set_name
)
loop
l_function := json_object_t();
l_function.put('name', r.function_name);
l_function.put('description', r.description);
l_parameters := json_object_t(r.parameters);
l_function.put('parameters', l_parameters);
l_functions.append(l_function);
end loop;
l_tool := json_object_t();
l_tool.put('function_declarations', l_functions);
l_tools := json_array_t();
l_tools.append(l_tool);
l_tools_clob := l_tools.to_clob();
else
l_tools_clob := null;
end if;
-- apex_debug.info(l_contents_clob);
utl_google_gemini_api.generate_content(
p_contents => l_contents_clob
,p_tools => l_tools_clob
,p_candidates => l_candidates
,p_prompt_feedback => l_prompt_feedback
,p_response => l_response
,p_credential_static_id => :G_CREDENTIAL
);
apex_debug.info(l_response);
l_part := utl_google_gemini_api.get_first_part(
p_candidates => l_candidates
,p_role => l_role
);
l_reply := l_part.get_string('text');
if l_reply is null then
/* append functionCall to chat history. */
apex_collection.add_member(
p_collection_name => 'GEMINI'
,p_c001 => l_role
,p_c002 => 'functionCall'
,p_clob001 => l_part.get_object('functionCall').to_clob()
);
/* call external function */
l_reply := utl_google_gemini_api.call_function(
p_part => l_part
);
/* append functionResponse to chat history */
apex_collection.add_member(
p_collection_name => 'GEMINI'
,p_c001 => 'function'
,p_c002 => 'functionResponse'
,p_clob001 => l_reply
);
else
apex_collection.add_member(
p_collection_name => 'GEMINI'
,p_c001 => l_role
,p_c002 => 'text'
,p_clob001 => l_reply
);
end if;
end;
サーバー側の条件ボタン押下時RUNを指定します。


レンダリング・ツリーに戻り、チャット履歴を表示するクラシック・レポートのリージョンを作成します。

識別タイトルChat HistoryソースタイプとしてSQL問合せを選択し、SQL問合せとして以下を記述します。
select c001, c002, clob001 from apex_collections where collection_name = 'GEMINI' order by seq_id

C001ヘッダーroleとします。


C002のヘッダーをtypeとします。


CLOB001タイプリッチ・テキストに変更します。ヘッダーtextです。設定書式としてマークダウンを選択します。


以上でアプリケーションは完成です。アプリケーションを実行すると、記事の先頭のGIF動画のように動作します。

今回作成したアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/google-gemini2.zip

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

2023年12月23日土曜日

Google Geminiを呼び出すAPEXアプリケーションを作る

Google AI Gemini APIを呼び出すAPEXアプリケーションを作ってみました。

以下のように動作します。Gemini APIはチャット形式のやり取り(OpenAIのChat Completionsとほぼ同じ)と関数呼び出し(OpenAIのFunction Callingとほぼ同じ)もサポートしていますが、これらの実装はまた別の機会にしようと思います。


写っている動物の質問に使った画像は以下です。


タヌキなんですがハクビシンといわれています。世界的にみると珍しい動物らしいので、学習されていないのでしょうか。

APEXのアプリケーションの作りは単純です。

Textのページは、質問を入力するテキスト領域のページ・アイテムであるP2_TEXT、質問を送信ボタンRUN、Geminiのレスポンスのマークダウン読み取り専用で表示するP2_RESPONSEから構成されています。


ボタンRUNを押した時に実行されるプロセスとして、以下のコードを記述します。ほとんどの処理はパッケージUTL_GOOGLE_GEMINI_APIで行っています。このコードは記事の末尾に添付しています。

declare
l_response clob;
l_candidates json_array_t;
l_prompt_feedback json_object_t;
l_role varchar2(8);
begin
utl_google_gemini_api.generate_content(
p_text => :P2_TEXT
,p_candidates => l_candidates
,p_prompt_feedback => l_prompt_feedback
,p_response => l_response
,p_credential_static_id => :G_CREDENTIAL
);
:P2_RESPONSE := utl_google_gemini_api.get_first_text(
p_candidates => l_candidates
,p_role => l_role
);
end;


画像の問い合わせをするページには、APEX 23.2で新設されたイメージ・アップロードを使っています。


Geminiを呼び出すプロセスのコードは以下になります。

declare
l_response clob;
l_candidates json_array_t;
l_prompt_feedback json_object_t;
l_role varchar2(8);
l_blob blob;
l_mime_type varchar2(100);
begin
select mime_type, blob_content into l_mime_type, l_blob
from apex_application_temp_files where name = :P3_IMAGE;
utl_google_gemini_api.generate_content(
p_text => :P3_TEXT
,p_image => l_blob
,p_mimetype => l_mime_type
,p_candidates => l_candidates
,p_prompt_feedback => l_prompt_feedback
,p_response => l_response
,p_credential_static_id => :G_CREDENTIAL
);
:P3_RESPONSE := utl_google_gemini_api.get_first_text(
p_candidates => l_candidates
,p_role => l_role
);
end;

今回作成したアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/google-gemini.zip

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


create or replace package utl_google_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(100) := 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent';
C_URL_GENERATE_CONTENT_VISION constant varchar2(100) := 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent';
C_URL_COUNT_TOKENS constant varchar2(100) := 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:countTokens';
C_URL_COUNT_TOKENS_VISION constant varchar2(100) := 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:countTokens';
C_URL_ENBED_CONTENT constant varchar2(100) := 'https://generativelanguage.googleapis.com/v1beta/models/embedding-001:embedContent';
/**
* 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;
/**
* 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_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_candidates out json_array_t
,p_prompt_feedback out json_object_t
,p_response out clob
,p_transfer_timeout in number default 180
/*
* providing to set p_transfer_timeout for apex_web_service is useful
* because 180sec is too long for some use cases.
*/
);
/**
* 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_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 4096
,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_candidates out json_array_t
,p_prompt_feedback out json_object_t
,p_response out clob
,p_transfer_timeout in number default 180
);
/**
* Text-and-image or movie, fileURI on Object Storage instead of blob.
* single-turn, model is gemini-pro-vision
*/
procedure generate_content(
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 4096
,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_candidates out json_array_t
,p_prompt_feedback out json_object_t
,p_response out clob
,p_transfer_timeout in number default 180
);
/**
* Text-only input, multi-turn, model is gemini-pro.
*/
procedure generate_content(
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_candidates out json_array_t
,p_prompt_feedback out json_object_t
,p_response out clob
,p_transfer_timeout in number default 180
);
/**
* Cout tokens.
*/
function count_tokens(
/* 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.
*/
function embed_content(
p_model in varchar2 default 'models/embedding-001'
,p_text in clob default null
,p_parts in clob default null /* for image */
,p_values out clob
,p_credential_static_id in varchar2
) return number;
end utl_google_gemini_api;
/
create or replace package body utl_google_gemini_api
as
/**
* 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;
/**
* 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_endpoint_url 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_candidates out json_array_t
,p_prompt_feedback out json_object_t
,p_response out clob
,p_transfer_timeout in number
)
as
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
-- 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 => p_endpoint_url
,p_http_method => 'POST'
,p_body => l_request_clob
,p_credential_static_id => p_credential_static_id
,p_transfer_timeout => p_transfer_timeout
);
if apex_web_service.g_status_code <> 200 then
raise e_api_call_failed;
end if;
p_response := l_response;
l_response_json := json_object_t(l_response);
p_candidates := l_response_json.get_array('candidates');
p_prompt_feedback := l_response_json.get_object('promptFeedback');
end generate_content;
/**
* Text-only input, single-trun, gemini-pro.
*/
procedure generate_content(
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_candidates out json_array_t
,p_prompt_feedback out json_object_t
,p_response out clob
,p_transfer_timeout in number
)
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('parts', l_parts);
l_contents.append(l_content);
l_contents_clob := l_contents.to_clob();
generate_content(
p_endpoint_url => C_URL_GENERATE_CONTENT
,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_candidates => p_candidates
,p_prompt_feedback => p_prompt_feedback
,p_response => p_response
,p_transfer_timeout => p_transfer_timeout
);
end generate_content;
/**
* Text-and-image, single-turn, gemini-pro-vision.
*/
procedure generate_content(
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_candidates out json_array_t
,p_prompt_feedback out json_object_t
,p_response out clob
,p_transfer_timeout in number
)
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('parts', l_parts);
l_contents.append(l_content);
l_contents_clob := l_contents.to_clob();
generate_content(
p_endpoint_url => C_URL_GENERATE_CONTENT_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_candidates => p_candidates
,p_prompt_feedback => p_prompt_feedback
,p_response => p_response
,p_transfer_timeout => p_transfer_timeout
);
end generate_content;
/**
* Text-and-image or movie, single-turn, gemini-pro-vision.
*/
procedure generate_content(
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_candidates out json_array_t
,p_prompt_feedback out json_object_t
,p_response out clob
,p_transfer_timeout in number
)
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_endpoint_url => C_URL_GENERATE_CONTENT_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_candidates => p_candidates
,p_prompt_feedback => p_prompt_feedback
,p_response => p_response
,p_transfer_timeout => p_transfer_timeout
);
end generate_content;
/**
* Text-only-input, multi-turn, gemini-pro.
*/
procedure generate_content(
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_candidates out json_array_t
,p_prompt_feedback out json_object_t
,p_response out clob
,p_transfer_timeout in number
)
as
l_contents json_array_t := json_array_t();
l_contents_clob clob;
l_response clob;
begin
generate_content(
p_endpoint_url => C_URL_GENERATE_CONTENT
,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_candidates => p_candidates
,p_prompt_feedback => p_prompt_feedback
,p_response => p_response
,p_transfer_timeout => p_transfer_timeout
);
end generate_content;
/**
* count tokens
*/
function count_tokens(
p_text in clob default null
,p_parts in clob default null
,p_credential_static_id in varchar2
) return number
as
l_url varchar2(200);
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_url := C_URL_COUNT_TOKENS;
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_url := C_URL_COUNT_TOKENS_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_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
* models/embedding-001 supports text only, p_parts can not be used.
*/
function embed_content(
p_model in varchar2 default 'models/embedding-001'
,p_text in clob
,p_parts in clob
,p_values out clob
,p_credential_static_id in varchar2
) return number
as
l_request json_object_t := json_object_t();
l_request_clob clob;
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_embedding json_object_t;
l_values json_array_t;
e_no_arguments exception;
e_api_call_failed exception;
begin
if p_text is not null then
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_parts := json_array_t(p_parts);
end if;
l_content.put('parts', l_parts);
l_request.put('model', p_model);
l_request.put('content', l_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 => C_URL_ENBED_CONTENT
,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_embedding := l_response_json.get_object('embedding');
l_values := l_embedding.get_array('values');
p_values := l_values.to_clob();
return l_values.get_size();
end embed_content;
end utl_google_gemini_api;
/

2023年12月22日金曜日

Autonomous Database Free Container ImageをmacOS上で実行する

オラクル社よりOracle Autonomous Database Free Container Imageという、Always FreeのAutonomous Databaseと同等のインスタンスをDockerやPodmanで動かせるイメージが提供されています。

Oracle Container Registryにイメージや導入手順の説明があります。

Other Open Source Licensesの箇所に、このContainer ImageがOracle Free Use Terms and Conditionsの元で配布されていると記載されています。


Oracle APEXのアプリケーション開発に使えるかと考えたのですが、Oracle APEXに関していうと、英語以外の言語リソースがインストールされていないため、使用は難しいというのが結論です。構成がAutonomous DatabaseであるためAPEXのスキーマが保護されていて、言語リソースの追加インストールもできません。

Oracle APEXの追加言語は開発ツールを多言語に対応させるためのものです。日本語などを含む作成済みのアプリケーションは言語リソースがインストールされていなくても、そのまま各国語の言語で動作します。アプリケーションを実行する環境としてであれば、日本語のアプリケーションを実行することはできます。

とはいえ、macOS上でAutonomous Database Free Container Imageを動作させたので、その手順を記録しておきます。

PodmanをインストールしたMacのスペックは以下です。すでにIntelのMacを持っている方は少ない気はします。


Podmanをインストールします。

brew install podman

すでにインストールされていたので、そのようなメッセージが表示されています。

% brew install podman

Warning: podman 5.0.2 is already installed and up-to-date.

To reinstall 5.0.2, run:

  brew reinstall podman

% 


Podmanの仮想マシンを作成します。

podman machine init

% podman machine init

Looking up Podman Machine image at quay.io/podman/machine-os:5.0 to create VM

Getting image source signatures

Copying blob 39d447e3fb35 done   | 

Copying config 44136fa355 done   | 

Writing manifest to image destination

39d447e3fb35b0626309d5e3591926284a4f1c370c2af5767b01350e3bf49d66

Extracting compressed file: podman-machine-default-amd64.raw: done  

Machine init complete

To start your machine run:


podman machine start


%


前出のOracle Container Registryのページの以下のFAQにしたがって、最低限のCPUとメモリを設定します。

How can I start podman VM on x86_64 Mac with minimum memory/cpu requirements ?

podman machine set --cpus 4 --memory 8192

% podman machine set --cpus 4 --memory 8192

% 

 
仮想マシンを起動します。

podman machine start

% podman machine start

Starting machine "podman-machine-default"


This machine is currently configured in rootless mode. If your containers

require root permissions (e.g. ports < 1024), or if you run into compatibility

issues with non-podman clients, you can switch using the following command:


podman machine set --rootful


API forwarding listening on: /var/run/docker.sock

Docker API clients default to this address. You do not need to set DOCKER_HOST.


Machine "podman-machine-default" started successfully

% 


仮想マシンの起動を確認します。

podman machine ls

% podman machine ls

NAME                     VM TYPE     CREATED        LAST UP            CPUS        MEMORY      DISK SIZE

podman-machine-default*  applehv     7 minutes ago  Currently running  4           8GiB        100GiB

% 


環境変数MYPASSとして、作成するコンテナに設定するパスワードをあらかじめ設定します。ウォレットのパスワードとユーザーADMINのパスワードに同じ値を設定します。

export MYPASS=[パスワード]

% export MYPASS=[パスワード] 

%


Autonomous Database Free Containerを実行します。作成されるコンテナの名前はadb-freeになります。23aiのコンテナ・イメージがリリースされる前は、パスワードを指定する環境変数にはMY_ATP_やMY_ADB_といった接頭辞が付けられていた覚えがあるのですが、現在はWALLET_PASSWORDADMIN_PASSWORDとなっています。
podman run -d \
-p 1521:1522 \
-p 1522:1522 \
-p 8443:8443 \
-p 27017:27017 \
-e WORKLOAD_TYPE=ATP \
-e WALLET_PASSWORD=$MYPASS \
-e ADMIN_PASSWORD=$MYPASS \
--cap-add SYS_ADMIN \
--device /dev/fuse \
--name adb-free \
container-registry.oracle.com/database/adb-free:latest-23ai

% podman run -d \

-p 1521:1522 \

-p 1522:1522 \

-p 8443:8443 \

-p 27017:27017 \

-e WORKLOAD_TYPE=ATP \

-e WALLET_PASSWORD=$MYPASS \

-e ADMIN_PASSWORD=$MYPASS \

--cap-add SYS_ADMIN \

--device /dev/fuse \

--name adb-free \

container-registry.oracle.com/database/adb-free:latest-23ai

Trying to pull container-registry.oracle.com/database/adb-free:latest-23ai...

Getting image source signatures

Copying blob sha256:40356c1517a23cf44e68a507194ff60153e4c578890d7122c7cc2295e4cd3144

Copying blob sha256:00ef0c1abdfeca2113126d07899a20378854f5c8c3a36163159c81bba6044529

Copying blob sha256:4fb9e1cce3a6abe0666107b18d5ac22dab4d9efb3bb4395a88839cb8d0bc5828

Copying config sha256:05353df50ddd0cfe86769904e643e30e71fc62d6a4780d75f1c37a727b0454b5

Writing manifest to image destination

96d0adb4a0445bbfc8b564ae67136c632279be94b2eb2562e21ab93f6ce2a39a

% 


環境変数MYPASSを消去します。

unset MYPASS

% unset MYPASS

% 


コンテナの実行を確認します。

podman container ps

% podman container ps

CONTAINER ID  IMAGE                                                        COMMAND     CREATED        STATUS                  PORTS                                                                                             NAMES

96d0adb4a044  container-registry.oracle.com/database/adb-free:latest-23ai              9 minutes ago  Up 9 minutes (healthy)  0.0.0.0:1521->1522/tcp, 0.0.0.0:1522->1522/tcp, 0.0.0.0:8443->8443/tcp, 0.0.0.0:27017->27017/tcp  adb-free

% 


手元のブラウザよりATPのAPEXにアクセスします。


自己署名証明書なので、接続にあたって確認を求められます。


警告を無視して接続します。ORDSのランディング・ページが表示されます。

Oracle APEXを実行します。


APEXの管理サービスへの接続画面が開きます。言語のセレクタがありません

podman run実行時にADMIN_PASSWORDとして指定したパスワードを入力し、管理サービスにサインインします。


作成済みにワークスペースが存在しないため、Create Workspaceのボタンが表示されます。

Create Workspaceをクリックします。


ワークスペースとして使用できるスキーマも存在しないため、New Schemaを選択します。


Workspace NameWorkspace UsernameWorkspace Passwordを設定し、Create Workspaceを実行します。


ワークスペースが作成されます。成功メッセージに含まれるワークスペース名のリンクをクリックして、ワークスペースのサインイン画面を開きます。


ワークスペース管理者のパスワードを入力し、ワークスペースにサインインします。言語リソースがロードされていないため、英語以外の言語の選択がありません。


APEXの開発画面が開きます。ここまで、非常にすばやく開発環境の作成ができています。言語リソースが含まれていないことが残念です。


コンテナadb-freeに接続してみます。

podman exec -it adb-free /bin/bash

 % podman exec -it adb-free /bin/bash

[oracle@740dfefffaaa /]$ 


PDBのMY_ATPに接続します。以下の環境変数を設定します。
export ORACLE_HOME=/u01/app/oracle/product/19.0.0.0/dbhome_1
export ORACLE_SID=POD1
export NLS_LANG=American_America.AL32UTF8
export PATH=$ORACLE_HOME/bin:$PATH
export TNS_ADMIN=/u01/app/oracle/wallets/tls_wallet

[oracle@96d0adb4a044 /]$ export ORACLE_HOME=/u01/app/oracle/product/23.0.0.0/dbhome_1

[oracle@96d0adb4a044 /]$ export ORACLE_SID=POD1

[oracle@96d0adb4a044 /]$ export NLS_LANG=American_America.AL32UTF8

[oracle@96d0adb4a044 /]$ export PATH=$ORACLE_HOME/bin:$PATH

[oracle@96d0adb4a044 /]$ export TNS_ADMIN=/u01/app/oracle/wallets/tls_wallet


ユーザーADMINで接続します。DATABASE_NAMEのデフォルトはMYATPなので、接続先のTNS名としてmyatp_lowを選びます。

sqlplus admin@myatp_low

[oracle@96d0adb4a044 /]$ sqlplus admin@myatp_low


SQL*Plus: Release 23.0.0.0.0 - Production on Mon May 13 00:19:08 2024

Version 23.4.0.24.05


Copyright (c) 1982, 2024, Oracle.  All rights reserved.


Enter password: ***********

Last Successful login time: Mon May 13 2024 00:17:19 +00:00


Connected to:

Oracle Database 23ai Enterprise Edition Release 23.0.0.0.0 - Production

Version 23.4.0.24.05


SQL> exit

Disconnected from Oracle Database 23ai Enterprise Edition Release 23.0.0.0.0 - Production

Version 23.4.0.24.05

[oracle@96d0adb4a044 /]$ 


exitで切断します。

[oracle@96d0adb4a044 /]$ exit

exit

% 


コンテナadb-freeを停止します。

podman container stop adb-free

% podman container stop adb-free

adb-free

% 


仮想マシンを停止します。

podman machine stop

% podman machine stop

Machine "podman-machine-default" stopped successfully

%


Oracle Autonomous Database Free Container ImageをmacOS上で動かした作業履歴は以上になります。

かえすがえすも言語リソースが入っていないことが残念です。