2024年4月25日木曜日

OpenAIのChat Completions APIを呼び出すAPEXアプリを作成する

以前にOpenAIChat Completions APIが使えるようになった頃に、同じテーマで記事を書いています。それから時間も経ち、OllamaLlama.cppといったOpenAIのChat Completions API互換のAPIをサポートするソフトウェアも利用できるようになりました。Chat Completions API自体には互換性があるため、これらを同様に扱えるひとつのAPEXアプリを作ってみることにしました。

とにかくChat Completions APIを呼び出せれば良いので、ユーザー・インターフェースの作り込みは最小限にします。APIの呼び出しはPL/SQLのパッケージUTL_OPENAI_CHAT_APIにまとめて実装しています。パッケージのコードは記事の末尾に添付します。パッケージUTL_OPENAI_CHAT_APIはツール呼び出しに関する実装も含んでいます。

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

以下よりAPEXアプリケーションの作成手順を紹介します。

今回はChat Completions APIを呼び出す対象をOpenAI、モデルは廉価なgpt-3.5-turboを使います。そのため、OpenAIのAPIキーをAPEXのWeb資格証明として作成しておきます。

ワークスペース・ユーティリティWeb資格証明を作成します。


Web資格証明名前OpenAI API Keyとしています。使用するWeb資格証明の指定には静的識別子を使用するため、この名前はなんでもかまいません。

静的識別子としてOPENAI_API_KEY認証タイプHTTPヘッダー資格証明名Authorizationとします。資格証明シークレットしてBearerで始めて空白で区切った後、OpenAIのAPIキーを続けた文字列を設定します。


アプリケーション・ビルダーからアプリケーションの作成を呼び出し、空のアプリケーションを作成します。

名前Chat with Generative AIとします。

アプリケーションの作成をクリックします。


空のアプリケーションが作成されます。Chat Completions APIを呼び出す機能はすべてホーム・ページに実装します。

最初に置換文字列に各種パラメータを設定します。アプリケーション定義の編集を開きます。


アプリケーション定義置換のセクションを開きます。

置換文字列として呼び出すサービスとモデルの指定に必要な情報を設定します。

呼び出す対象はOpenAIなので、G_API_ENDPOINTとしてhttps://api.openai.com/v1/chat/completionsを設定します。G_CHAT_HISTORYにはチャット履歴を保存するAPEXコレクションの名前としてCHAT_HISTORYを設定します。これはアプリケーション内で同じ名前のコレクションを参照させるためのもので、どのような名前でも構いませんが、あえて変更する必要もないでしょう。G_MODEL_NAMEgpt-3.5-turboを指定します。G_CREDENTIALとしてOpenAIのAPIキーを登録したWeb資格証明であるOPENAI_API_KEYを設定します。

以上を設定して、変更の適用をクリックします。


ページ・デザイナホーム・ページを開きます。

最初にチャット履歴を初期化するボタンを作成します。ボタンはブレッドクラムのリージョンChat with Generative AIに作成します。

識別ボタン名INITラベルStart New Conversationとします。レイアウト位置Nextを選択します。

動作アクションとしてこのアプリケーションのページにリダイレクトを選択します。


ターゲットをクリックしリンク・ビルダーを開きます。

ターゲットページ&APP_PAGE_ID.を記述します。これはこのボタンがあるページ番号、つまり自ページの呼び出しを指定しています。詳細リクエストINIT_CONVERSATIONを設定します。

OKをクリックし、リンク・ビルダーを閉じます。


このボタンを押した時に実行されるプロセスを作成します。

レンダリング前ヘッダーの前にプロセスを作成します。

識別名前Init Conversationとします。タイプコードの実行です。ソースPL/SQLコードとして以下を記述します。APEXコレクションが作成されていないか(サインイン直後)、またはREQUESTにINIT_CONVERSATIONが設定されている場合(ボタンStart New Conversationが押されたとき)に、APEXコレクションを初期化します。最初にロールがsystemの空のプロンプトを挿入します。このsystemプロンプトは変更できるように、画面にUIを実装します。

begin
if not apex_collection.collection_exists(:G_CHAT_HISTORY) or :REQUEST = 'INIT_CONVERSATION' then
/*
* チャット履歴を初期化する。
* 空のsystemロールのメッセージを最初に登録する。メッセージがからのままの場合、Chat Completions APIへの
* リエクエストにsystemロールのメッセージ自体を含めない。
*/
apex_collection.create_or_truncate_collection(:G_CHAT_HISTORY);
apex_collection.add_member(
p_collection_name => :G_CHAT_HISTORY
,p_c001 => 'system'
,p_clob001 => ''
);
:P1_REQUEST := '';
:P1_RESPONSE := '';
end if;
end;


systemプロンプトを設定するUIを作成します。

新規にリージョンを作成します。

識別タイトルSystem Promptとします。タイプ静的コンテンツです。


作成したリージョンに、プロンプトを入力するページ・アイテムを作成します。

識別名前P1_PROMPTタイプテキスト領域とします。ラベルPromptです。セッション・ステートデータ型CLOBストレージセッションごと(永続)を選択します。


入力したsystemプロンプトをチャット履歴に設定するボタンを作成します。

識別ボタン名SET_PROMPTラベルSet Promptとします。動作アクションはデフォルトのページの送信です。


ボタンSET_PROMPTをクリックしたときに実行するプロセスを作成します。

左ペインでプロセス・ビューを開き、新規にプロセスを作成します。

識別名前Set Promptとします。タイプコードを実行です。ソースPL/SQLコードとして以下を記述します。すでに登録済みのsystemプロンプトを置き換えます。

declare
l_seq number;
begin
/*
* 登録済みのsystemプロンプトの位置を求める。
* コレクション初期化直後に空のsystemプロンプトが作成済みなので、通常は先頭にある。
*/
select seq_id into l_seq from apex_collections where collection_name = :G_CHAT_HISTORY and c001 = 'system';
/* プロンプトを更新する */
apex_collection.update_member(
p_collection_name => :G_CHAT_HISTORY
,p_seq => l_seq
,p_c001 => 'system'
,p_clob001 => :P1_PROMPT
);
end;
view raw set_prompt.sql hosted with ❤ by GitHub

サーバー側の条件ボタン押下時SET_PROMPTを指定します。


ユーザーによるメッセージを送信するUIを作成します。

新規にリージョンを作成します。

識別タイトルUser Messageとします。タイプ静的コンテンツです。


作成したリージョンに、ユーザーによるメッセージを入力するページ・アイテムを作成します。

識別名前P1_MESSAGEタイプテキスト領域とします。ラベルMessageです。セッション・ステートデータ型CLOBストレージリクエストごと(メモリーのみ)を選択します。


入力したメッセージを含めて、Chat Completions APIを呼び出すボタンを作成します。

識別ボタン名SEND_MESSAGEラベルSend Messageとします。動作アクションはデフォルトのページの送信です。


Chat Completions APIのレスポンスを含めた、チャット履歴を表示するリージョンを作成します。

識別タイトルChat Historyタイプカードを選びます。ソースのタイプSQL問合せを選択し、SQL問合せとして以下を記述します。
select seq_id, c001, c002, clob001, n001, n002, n003
from apex_collections
where collection_name = :G_CHAT_HISTORY order by seq_id desc

カードに表示する内容を設定します。プロパティ・エディタ属性タブを開きます。

外観レイアウトとして水平(行)を選びます。カード主キー列1SEQ_IDです。

タイトルC001サブタイトルC002本体CLOB001を選択します。2次本体拡張フォーマットオンにし、HTML式として以下を記述します。
{if N001/}
prompt_tokens: &N001. completion_tokens: &N002. total_tokens: &N003.
{endif/}

デバッグ用にChat Completions APIに送信したリクエストと、受信したレスポンスを表示するページ・アイテムを作成します。

送信したリクエストを保持するページ・アイテムはP1_REQUESTとして作成します。ラベルRequest設定ページの送信時に送信オフにします。セッション・ステートデータ型CLOBストレージセッションごと(永続)を選択します。


同様にレスポンスを保持するページ・アイテムP1_RESPONSEを作成します。ラベルResponseとします。


左ペインでプロセス・ビューを開き、ボタンSEND_MESSAGEをクリックしたときに実行されるプロセスを作成します。

識別名前Send MessageタイプAPIの呼出しとします。設定パッケージUTL_OPENAI_CHAT_APIプロシージャまたはファンクションCHATを選択します。

サーバー側の条件ボタン押下時SEND_MESSAGEを指定します。


パラメータp_contentタイプアイテムアイテムP1_MESSAGEとします。


パラメータp_collection_nameタイプ静的値静的値&G_CHAT_HISTORY.とします。


パラメータp_api_endpointタイプ静的値静的値&G_API_ENDPOINT.とします。


パラメータp_model_nameタイプ静的値静的値&G_MODEL_NAME.とします。


パラメータp_credential_static_idタイプ静的値静的値&G_CREDENTIAL.とします。


パラメータp_request_outの値のアイテムにP1_REQUESTを指定します。


パラメータp_response_outの値のアイテムにP1_RESPONSEを指定します。


その他のパラメータはAPIデフォルトのまま変更しません。

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

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

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


コードにはツール呼び出しの対応が含まれていて、ツールに関する定義を保存する表OPENAI_TOOLSへの参照があります。そのため、以下のDDLを実行して表をあらかじめ作成しておく必要があります。

CREATE TABLE "OPENAI_TOOLS"
( "ID" NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY MINVALUE 1 MAXVALUE 9999999999999999999999999999 INCREMENT BY 1 START WITH 1 CACHE 20 NOORDER NOCYCLE NOKEEP NOSCALE NOT NULL ENABLE,
"TOOL_SET" VARCHAR2(20 CHAR) NOT NULL ENABLE,
"TOOL_TYPE" VARCHAR2(20 CHAR) DEFAULT ON NULL 'function' NOT NULL ENABLE,
"DESCRIPTION" VARCHAR2(4000 CHAR) NOT NULL ENABLE,
"TOOL_NAME" VARCHAR2(80 CHAR) NOT NULL ENABLE,
"PARAMETERS" CLOB,
CHECK (parameters is json) ENABLE,
CONSTRAINT "OPENAI_TOOLS_ID_PK" PRIMARY KEY ("ID")
USING INDEX ENABLE
) ;


create or replace package utl_openai_chat_api as
/**
* Change History:
* 2024/11/29 - ynakakos change name and strict included in the json_schema to arguments.
* 2024/10/29 - ynakakos add OpenAI Structured Outputs support.
*/
/**
* ツールセット名からJSON形式のツール定義を生成する。
*
* @param p_tool_set ツールセット名
* @return json_array_t toolsとして送信するファンクション定義
*/
function generate_tools(
p_tool_set in varchar2
) return json_array_t;
/**
* OpenAI Chat Completions APIおよび互換APIを呼び出す。
* OpenAIのエンドポイント https://api.openai.com/v1/chat/completionsと互換ががあること。
*
* @param p_content メッセージ本文
* @param p_collection_name チャット履歴を保存するAPEXコレクションの名前
* @param p_api_endpoint Chat Completions APIを呼び出すURL
* @param p_model_name モデル名(必須)
* @param p_max_tokens トークン数の上限
* @param p_stream streamingの許可 - APEXはstreamingが扱えないためデフォルトでfalse
* @param p_credential_static_id Web資格証明の静的ID
* @param p_tool_set 表GENAI_TOOLSのTOO_SET列の名前
* @param p_response_format text、json_object または json_schema
* @param p_json_schema_name respons_formatがjson_schematのときのname属性の値
* @param p_json_schema_strict 同上のstrict属性の値
* @param p_json_schema 同上のschema属性の値 - リクエストに含めるJSON schema
* @param p_request_out デバッグ用 - Chat APIに送信したリクエスト本文
* @param p_response_out デバッグ用 - Chat APIから受信したレスポンス本文
* @param p_transfer_timeout Chat APIの応答のタイムアウト - 秒
* @param p_recursive_calL_count ツール呼び出しを行った後に許可する再帰呼び出しの回数。
*/
procedure chat(
p_content in clob default null
,p_collection_name in varchar2
,p_api_endpoint in varchar2
,p_model_name in varchar2
,p_max_tokens in number default null
,p_stream in boolean default false
,p_credential_static_id in varchar2 default null
/* function calling & Structured Outputs 向け */
,p_tool_set in varchar2 default null
,p_response_format in varchar2 default null
/* p_response_formatがjson_schemaのときに有効 */
,p_json_schema_name in varchar2 default null
,p_json_schema_strict in boolean default true
,p_json_schema in clob default null
/* 以下、デバッグ用 */
,p_request_out out clob
,p_response_out out clob
/* 無視しても良いパラメータ */
,p_transfer_timeout in number default 360 -- 10 min
,p_recursive_call_count in number default 0
);
end utl_openai_chat_api;
/
create or replace package body utl_openai_chat_api as
/**
* APEXコレクションに保存されたチャット履歴からメッセージを生成する。
* ツールからの応答だけを送信する場合は、p_contentをnullにする。
*
* @param p_collection_name チャット履歴を保存するAPEXコレクション名
* @param p_role チャット履歴の最後に追加するメッセージのロール - デフォルトuser
* @param p_content チャット履歴の最後の追加するメッセージ - デフォルトnull
* @return json_array_t 送信するメッセージの配列
*/
function init_message_from_collection(
p_collection_name in varchar2
,p_role in varchar2 default 'user'
,p_content in clob default null
)
return json_array_t
as
l_messages json_array_t;
l_message json_object_t;
begin
/*
* ユーザーからの入力があれば、Chat履歴の最後に追加する。
*/
if p_content is not null then
apex_collection.add_member(
p_collection_name => p_collection_name
,p_c001 => p_role
,p_clob001 => p_content
);
end if;
/*
* 生成AIのChat APIに送信するメッセージを作成する。
  * APEXコレクションより、作成順の昇順でメッセージの配列にする。
*/
l_messages := json_array_t();
for r in (
/*
* contentが空であればメッセージに含めない。
* ロールがsystemでは、メッセージが空のときがある。
*/
select c001, c002, c003, clob001 from apex_collections
where collection_name = p_collection_name and dbms_lob.getlength(clob001) > 0
order by seq_id
)
loop
l_message := json_object_t();
l_message.put('role' ,r.c001);
if r.c001 = 'tool' then -- c001はrole
l_message.put('tool_call_id', r.c002);
l_message.put('name', r.c003);
end if;
if r.c002 = 'tool_calls' then -- c002はfinish_reason
l_message.put('tool_calls', json_array_t(r.clob001));
else
l_message.put('content', r.clob001);
end if;
l_messages.append(l_message);
end loop;
return l_messages;
end init_message_from_collection;
/**
* ツールセット名からJSON形式のツール定義を生成する。
*/
function generate_tools(
p_tool_set in varchar2
) return json_array_t
as
l_tools json_array_t;
l_tool json_object_t;
l_function json_object_t;
l_parameters json_object_t;
begin
l_tools := json_array_t();
for r in (
/* ツールの指定は表OPENAI_TOOLSに保存されている。 */
select * from openai_tools where tool_set = p_tool_set
)
loop
if r.tool_type = 'function' then
/* only function is supported as of 19, Apr. 2024 */
l_tool := json_object_t();
l_tool.put('type','function');
l_function := json_object_t();
l_function.put('name', r.tool_name);
l_function.put('description', r.description);
/* parametersにはJSON Schemaがそのまま設定されている */
l_parameters := json_object_t(r.parameters);
l_function.put('parameters', l_parameters);
l_tool.put('function', l_function);
l_tools.append(l_tool);
end if;
end loop;
return l_tools;
end generate_tools;
/**
* メッセージにツールの指定を含める。
*
* @param p_request Chat APIのリクエスト本文 in/out
* @param p_tool_set toolとして含めるツールを選択するtool_set名(GENAI_TOOLS.TOOL_SET)
* @param p_response_format text、json_object または json_schema
* @param p_json_schema_name respons_formatがjson_schematのときのname属性の値
* @param p_json_schema_strict 同上のstrict属性の値
* @param p_json_schema 同上のschema属性の値 - リクエストに含めるJSON schema
*/
procedure configure_tools(
p_request in out json_object_t
,p_tool_set in varchar2 default null
,p_response_format in varchar2 default null
,p_json_schema_name in varchar2 default null
,p_json_schema_strict in boolean default true
,p_json_schema in clob default null
)
as
l_tools json_array_t;
l_response_format json_object_t;
l_json_schema json_object_t;
begin
/*
* p_tool_setの指定があれば、toolの定義を送信する。
*/
if p_tool_set is not null then
l_tools := generate_tools(
p_tool_set => p_tool_set
);
p_request.put('tools', l_tools);
end if;
/* response_format */
if p_response_format is not null then
l_response_format := json_object_t();
l_response_format.put('type', p_response_format); /* text, json_object or json_schema */
if p_response_format = 'json_schema' then
/*
* json_schemaとして与えられたJSON SchemaをStructured Outputsに
* 必要なname, schema, strictでラップする。
*/
l_json_schema := json_object_t();
l_json_schema.put('name', p_json_schema_name);
l_json_schema.put('schema', json_object_t(p_json_schema));
l_json_schema.put('strict', p_json_schema_strict);
l_response_format.put('json_schema', l_json_schema);
end if;
p_request.put('response_format', l_response_format);
end if;
end configure_tools;
/**
* ツール呼び出し(function calling)の処理を行う。
* tool_callsを解釈しファンクションを呼び出し、ロールをtoolとして呼び出したファンクションの応答を
* APEXコレクションに追記する。
*
* @param p_collectiuon_name Chat履歴を保存するAPEXコレクション名
* @param p_message tool_callsを含んだレスポンス本文
*/
procedure process_tool_calls(
p_collection_name in varchar2
,p_message in json_object_t
)
as
l_tool_calls json_array_t;
l_tool_call json_object_t;
l_tool_call_id varchar2(80);
l_function_call json_object_t;
l_function_name varchar2(160);
l_function_args clob;
l_function_arg_obj json_object_t;
l_dynamic_sql varchar2(4000);
l_function_out clob;
begin
l_tool_calls := p_message.get_array('tool_calls');
for i in 1..l_tool_calls.get_size()
loop
l_tool_call := treat(l_tool_calls.get(i-1) as json_object_t);
if l_tool_call.get_string('type') = 'function' then
/* only function is supported as type */
l_tool_call_id := l_tool_call.get_string('id');
l_function_call := l_tool_call.get_object('function');
if l_function_call is not null then
l_function_name := l_function_call.get_string('name');
l_function_args := l_function_call.get_clob('arguments');
if l_function_args is null then
/* OpenAI returns arguments as CLOB but could be json_object */
l_function_arg_obj := l_function_call.get_object('arguments');
if l_function_arg_obj is not null then
l_function_args := l_function_arg_obj.to_clob();
end if;
end if;
/* ストアド・プロシージャを動的に呼び出す。 */
apex_debug.info('Calling %s with %s', l_function_name, l_function_args);
l_dynamic_sql := 'begin :a := ' || l_function_name || '(:b); end;';
execute immediate l_dynamic_sql using in out l_function_out, l_function_args;
apex_collection.add_member(
p_collection_name => p_collection_name
,p_c001 => 'tool'
,p_c002 => l_tool_call_id
,p_c003 => l_function_name
,p_clob001 => l_function_out
);
end if;
end if;
end loop;
end process_tool_calls;
/**
* Batch APIの呼び出しも使えるように、メッセージの作成部分をプロシージャCHATより
* 分離した。
*/
function generate_chat_message(
p_content in clob
,p_collection_name in varchar2
,p_model_name in varchar2
,p_max_tokens in number
,p_stream in boolean
/* function calling向け */
,p_tool_set in varchar2
,p_response_format in varchar2
,p_json_schema_name in varchar2
,p_json_schema_strict in boolean
,p_json_schema in clob
)
return clob
as
l_request json_object_t;
l_request_clob clob;
l_messages json_array_t;
l_message json_object_t;
begin
/*
* LLMに送信するメッセージをl_requestとして作成する。
*/
l_request := json_object_t();
/* modelは必ず必要 */
l_request.put('model', p_model_name);
if p_max_tokens is not null then
l_request.put('max_tokens', p_max_tokens);
end if;
/* APEXではstreamingは処理できないので、基本常にfalse */
if p_stream is not null then
l_request.put('stream', p_stream);
end if;
/*
* APIで送信するメッセージをチャット履歴から初期化する。
*/
l_messages := init_message_from_collection(
p_collection_name => p_collection_name
,p_content => p_content
);
l_request.put('messages', l_messages);
/*
* ツール・セットp_tool_setの指定があれば、toolの構成を
* リクエストに含める。
*/
configure_tools(
p_request => l_request
,p_tool_set => p_tool_set
,p_response_format => p_response_format
,p_json_schema_name => p_json_schema_name
,p_json_schema_strict => p_json_schema_strict
,p_json_schema => p_json_schema
);
/*
* temprature, top_pなどのパラメータを設定するとしたら、ここでputする。
*/
/*
* 生成AIのChat APIを呼び出す。
*/
l_request_clob := l_request.to_clob();
return l_request_clob;
end generate_chat_message;
/**
* Chat APIの呼び出し。
*/
procedure chat(
p_content in clob
,p_collection_name in varchar2
,p_api_endpoint in varchar2
,p_model_name in varchar2
,p_max_tokens in number
,p_stream in boolean
,p_credential_static_id in varchar2
/* function calling向け */
,p_tool_set in varchar2
,p_response_format in varchar2
,p_json_schema_name in varchar2
,p_json_schema_strict in boolean
,p_json_schema in clob
/* 以下、デバッグ用 */
,p_request_out out clob
,p_response_out out clob
/* 無視しても良いパラメータ */
,p_transfer_timeout in number
,p_recursive_call_count in number
)
as
l_request_clob clob;
l_message json_object_t;
l_response_clob clob;
l_response json_object_t;
l_choices json_array_t;
l_choice0 json_object_t;
l_role varchar2(80);
l_content clob;
l_usage json_object_t;
/* usage */
l_n001 number;
l_n002 number;
l_n003 number;
/* response_format */
l_response_format json_object_t;
l_finish_reason varchar2(4000);
e_to_many_recursive_calls exception;
/* Ollama Functiion Calling */
l_tools_string clob;
l_tools_obj json_object_t;
l_seq number;
r_member apex_collections%rowtype;
/* JSON構文エラー */
e_bad_json_syntax exception;
pragma exception_init(e_bad_json_syntax, -40441);
begin
/*
* ツールの再帰呼び出しは1回までに限定する。
*/
if p_recursive_call_count > 1 then
raise e_to_many_recursive_calls;
end if;
/*
* 送信メッセージの作成。
*/
l_request_clob := generate_chat_message(
p_content => p_content
,p_collection_name => p_collection_name
,p_model_name => p_model_name
,p_max_tokens => p_max_tokens
,p_stream => p_stream
,p_tool_set => p_tool_set
,p_response_format => p_response_format
,p_json_schema_name => p_json_schema_name
,p_json_schema_strict => p_json_schema_strict
,p_json_schema => p_json_schema
);
p_request_out := l_request_clob; -- デバッグ用
/*
* Chat Completions APIの呼び出し。
*/
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Content-Type','application/json',p_reset => false);
l_response_clob := apex_web_service.make_rest_request(
p_url => p_api_endpoint
,p_http_method => 'POST'
,p_body => l_request_clob
,p_credential_static_id => p_credential_static_id
,p_transfer_timeout => p_transfer_timeout
);
p_response_out := l_response_clob;
l_response := json_object_t(l_response_clob);
/*
* 生成AIからの応答を処理する。
*/
l_choices := l_response.get_array('choices');
if l_choices is not null then
/* OpenAI flavor */
l_choice0 := treat(l_choices.get(0) as json_object_t);
l_message := l_choice0.get_object('message');
/* finish_reaonは、後で確認する。 */
l_finish_reason := l_choice0.get_string('finish_reason');
else
/* Ollama flavor */
l_message := l_response.get_object('message');
end if;
l_role := l_message.get_string('role');
if l_finish_reason = 'tool_calls' then
l_content := l_message.get_array('tool_calls').to_clob();
else
l_content := l_message.get_clob('content');
end if;
/* usageも取り出す。 */
l_usage := l_response.get_object('usage');
if l_usage is not null then
/* OpenAI flavor */
l_n001 := l_usage.get_number('prompt_tokens');
l_n002 := l_usage.get_number('completion_tokens');
l_n003 := l_usage.get_number('total_tokens');
else
/* Ollama flavor */
l_n001 := l_response.get_number('prompt_eval_count');
l_n002 := l_response.get_number('eval_count');
end if;
/*
* 生成AIからの応答をAPEXコレクションに追記する。
*
* response_formatの指定がjson_objectまたはjson_schemaの
* 場合は、pretty printする。
*/
if p_response_format in ('json_object', 'json_schema') then
begin
select json_serialize(l_content pretty) into l_content from dual;
exception
when e_bad_json_syntax then
/* 出力を見ると分かるので、構文エラーは無視 */
null;
end;
end if;
apex_collection.add_member(
p_collection_name => p_collection_name
,p_c001 => l_role
,p_c002 => l_finish_reason
,p_clob001 => l_content
,p_n001 => l_n001
,p_n002 => l_n002
,p_n003 => l_n003
);
/*
* finish_reasonがtool_callsであれば、指定されたツールを呼び出す。
*/
if l_finish_reason = 'tool_calls' then
process_tool_calls(
p_collection_name => p_collection_name
,p_message => l_message
);
/*
* ツールを呼び出した結果をLLMに送信する。
*/
chat(
p_collection_name => p_collection_name
,p_api_endpoint => p_api_endpoint
,p_model_name => p_model_name
,p_max_tokens => p_max_tokens
,p_stream => p_stream
,p_credential_static_id => p_credential_static_id
,p_tool_set => p_tool_set
,p_response_format => p_response_format
,p_request_out => p_request_out
,p_response_out => p_response_out
,p_transfer_timeout => p_transfer_timeout
,p_recursive_call_count => (p_recursive_call_count + 1)
);
end if;
end chat;
end utl_openai_chat_api;
/