以前にOpenAIのChat Completions APIが使えるようになった頃に、同じテーマで記事を書いています。それから時間も経ち、OllamaやLlama.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_NAMEはgpt-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を実装します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 Promptとします。タイプは静的コンテンツです。
作成したリージョンに、プロンプトを入力するページ・アイテムを作成します。
識別の名前はP1_PROMPT、タイプはテキスト領域とします。ラベルはPromptです。セッション・ステートのデータ型はCLOB、ストレージにセッションごと(永続)を選択します。
入力したsystemプロンプトをチャット履歴に設定するボタンを作成します。
識別のボタン名はSET_PROMPT、ラベルはSet Promptとします。動作のアクションはデフォルトのページの送信です。
ボタンSET_PROMPTをクリックしたときに実行するプロセスを作成します。
左ペインでプロセス・ビューを開き、新規にプロセスを作成します。
識別の名前はSet Promptとします。タイプはコードを実行です。ソースのPL/SQLコードとして以下を記述します。すでに登録済みのsystemプロンプトを置き換えます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare | |
l_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; |
サーバー側の条件のボタン押下時に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
カードに表示する内容を設定します。プロパティ・エディタの属性タブを開きます。
外観のレイアウトとして水平(行)を選びます。カードの主キー列1はSEQ_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_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を実行して表をあらかじめ作成しておく必要があります。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
CREATE 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 | |
) ; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
create or replace package utl_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; | |
/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
create or replace package 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; | |
/ |