2023年12月5日火曜日

OpenAIのAssistants APIを呼び出すアプリのツールにCode InterpreterとFunction Callingを追加する

先日作成したOpenAI Assistants APIを呼び出してRetrievalを実行するAPEXアプリケーションに、ツールとしてCode InterpreterとFunction Callingを追加してみました。Function Callingを呼び出すアプリケーションは以前に「OpenAI ChatGPTのFunction Callingを使ってPL/SQLプロシージャを呼び出す」として記事にしています。Function Callingに関しては、この記事の実装を流用します。

Function Callingを使って東京の天気を問い合わせています。


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

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

クイックSQLの以下のモデルから、表OPENAI_FUNCTIONSOPENAI_FUNCTION_SETを作成します。

# prefix: openai
functions
function_name vc200 /nn
description vc4000 /nn
parameters json /nn
function_set
function_set_name vc80 /nn
function_id num /nn /fk functions

ページ作成ウィザードを起動し、対話モード・レポートを選択します。表OPENAI_FUNCTIONSフォーム付き対話モード・レポートのページを作成します。

対話モード・レポートページ番号98は別のページで使うので空けておきます)、名前Functionsフォームフォーム・ページ番号10フォーム・ページ名Function Detailとします。データ・ソース表/ビューの名前OPENAI_FUNCTIONSを指定します。

それ以外はデフォルトでに進み、主キー列1ID (Number)を指定してページの作成を実行します。


同様に表OPENAI_FUNCTION_SETフォーム付き対話モード・レポートのページを作成します。

対話モード・レポートページ番号11名前Function Setフォームフォーム・ページ番号12フォーム・ページ名Function Set Detailとします。データ・ソース表/ビューの名前OPENAI_FUNCTION_SETを指定します。

それ以外はデフォルトでに進み、主キー列1ID (Number)を指定してページの作成を実行します。


OPENAI_FUNCTIONSOPENAI_FUNCTION_SETを編集するページができたので、記事「OpenAI ChatGPTのFunction Callingを使ってPL/SQLプロシージャを呼び出す」を参照して、PL/SQLファンクションGET_CURRENT_WEATERを作成します。表OPENAI_FUNCTIONSにファンクションとしてget_current_weatherを登録し、表OPENAI_FUNCTION_SETにファンクションget_current_weaterを含んだFunction SetとしてWeatherを作成します。

アシスタントを作成する際に上記のファンクションセットを選択できるように、共有コンポーネントLOVを作成します。

作成するLOVの名前LOV_FUNCTION_SETとします。タイプDynamicです。

へ進みます。


表ビューの名前としてOPENAI_FUNCTION_SET (表)を選択します。

へ進みます。


戻り列ID表示列FUNCTION_SET_NAMEです。

作成をクリックすると、LOV_FUNCTION_SETが作成されます。


Function Callingを呼び出す際に使用するパッケージUTL_OPENAI_TOOLS_APIを作成します。以下のパッケージのコードを実行します。

create or replace package utl_openai_tools_api
as
/**
* アシスタントを作成する引数toolsとして与えるJSONを生成する。
*
* @param p_code_interpreter Code Interpreterを含む場合は真
* @param p_retrieval Retrievalを含む場合は真
* @param p_function_set_id 表OPENAI_FUNCTION_SETのID値。
* 指定があればfunction callingの設定を含める。
* return toolsに与えるJSON
*/
function generate_tools_json(
p_code_interpreter in boolean
,p_retrieval in boolean
,p_function_set_id in openai_function_set.id%type
)
return clob;
/**
* Statusがrequire_actionのRunであることを確認し、tool_callsを取り出し
* 指定されてPL/SQLプロシージャの呼び出しを行う。
*
* @param p_run_id run id
* @param p_thread_id thread id
* @param p_credential_static_id OpenAIのAPI呼び出しに使用するWeb資格証明の静的ID
* @param p_required_action Runオブジェクトのrequired_action - デバッグ用
* @param p_tool_outputs PL/SQLプロシージャを呼び出しsubmit_tool_outputsとして送信した内容 - デバッグ用
* @param p_response submit_tool_outputsの応答 - デバッグ用
*/
procedure submit_tool_outputs(
p_run_id in varchar2
,p_thread_id in varchar2
,p_credential_static_id in varchar2 default 'OPENAI_API_KEY'
,p_required_action out clob
,p_tool_outputs out clob
,p_response out clob
);
end utl_openai_tools_api;
/
create or replace package body utl_openai_tools_api
as
function generate_tools_json(
p_code_interpreter in boolean
,p_retrieval in boolean
,p_function_set_id in openai_function_set.id%type
)
return clob
as
l_tool json_object_t;
l_tools json_array_t;
l_tools_clob clob;
l_function json_object_t;
l_parameters json_object_t;
begin
l_tools := json_array_t();
/*
* Code Interpreterが有効。
*/
if p_code_interpreter then
l_tool := json_object_t('{ "type": "code_interpreter" }');
l_tools.append(l_tool);
end if;
/*
* Retrievalが有効。
*/
if p_retrieval then
l_tool := json_object_t('{ "type": "retrieval" }');
l_tools.append(l_tool);
end if;
/*
* function setの指定がある。
*/
if p_function_set_id is not null then
for r in (
select * from openai_functions f join openai_function_set s on f.id = s.function_id
where s.id = p_function_set_id
)
loop
l_tool := json_object_t('{ "type": "function" }');
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_tool.put('function', l_function);
l_tools.append(l_tool);
end loop;
end if;
l_tools_clob := l_tools.to_clob();
return l_tools_clob;
end generate_tools_json;
procedure submit_tool_outputs(
p_run_id in varchar2
,p_thread_id in varchar2
,p_credential_static_id in varchar2
,p_required_action out clob
,p_tool_outputs out clob
,p_response out clob -- Modified run object
)
as
l_run_clob clob;
l_run json_object_t;
l_status varchar2(80);
l_required_action json_object_t;
l_submit_tool_outputs json_object_t;
l_tool_calls json_array_t;
l_call json_object_t;
l_function json_object_t;
/* 動的SQL呼び出し */
l_function_name varchar2(200);
l_function_args clob;
l_dynamic_sql varchar2(32767);
l_function_out clob;
l_tool_outputs json_array_t;
l_tool_output json_object_t;
/* OpenAI Assistants APIの呼び出し */
l_url varchar2(400);
l_request_json json_object_t;
l_request clob;
l_response clob;
l_response_json json_object_t;
e_api_call_failed exception;
begin
/*
* 指定されたRunのStatusがrequires_actionかどうか確認する。
*/
l_url := 'https://api.openai.com/v1/threads/' || p_thread_id || '/runs/' || p_run_id;
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('OpenAI-Beta','assistants=v1',p_reset => false);
apex_web_service.set_request_headers('Content-Type','application/json',p_reset => false);
l_run_clob := apex_web_service.make_rest_request(
p_url => l_url
,p_http_method => 'GET'
,p_credential_static_id => p_credential_static_id
);
if apex_web_service.g_status_code <> 200 then
apex_debug.info('status = %s, reseponse = %s', apex_web_service.g_status_code, l_run_clob);
raise e_api_call_failed;
end if;
l_run := json_object_t(l_run_clob);
l_status := l_run.get_string('status');
if l_status <> 'requires_action' then
/* 異なっている場合は何もしないで終了する */
apex_debug.info('status is not requires_action, exit. %s', l_run_clob);
return;
end if;
/*
* Statusがrequires_actionであれば
* typeがsubmit_tool_outputsのrequired_actionがあるはず。
* それを取り出す。
*/
l_required_action := l_run.get_object('required_action');
p_required_action := l_required_action.to_clob();
apex_debug.info('required_action %s', p_required_action);
if l_required_action.get_string('type') <> 'submit_tool_outputs' then
/*
* typeがsubmit_tool_outputsでなければ何もしない。
* l_required_actionに値があるのにl_tool_outputに結果が返されない状態になる。
*/
apex_debug.info('type is not submit_tool_outputs, exit.');
return;
end if;
l_submit_tool_outputs := l_required_action.get_object('submit_tool_outputs');
l_tool_calls := l_submit_tool_outputs.get_array('tool_calls');
l_tool_outputs := json_array_t();
for i in 1..l_tool_calls.get_size()
loop
l_call := treat(l_tool_calls.get(i-1) as json_object_t);
if l_call.get_string('type') = 'function' then
/*
* 今の所Function Callingのtypeはfunctionのみなはず。
* そうでなければRunはrequires_actionのステータスで停止する。
*/
l_function := l_call.get_object('function');
l_function_name := l_function.get_string('name');
l_function_args := l_function.get_clob('arguments');
/* ストアド・プロシージャを動的に呼び出す。 */
apex_debug.info('function calling name %s, args %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;
/* 実行結果をtool_outputsに追加 */
apex_debug.info('function calling result %s', l_function_out);
l_tool_output := json_object_t();
l_tool_output.put('tool_call_id', l_call.get_string('id'));
l_tool_output.put('output', l_function_out);
l_tool_outputs.append(l_tool_output);
end if;
end loop;
/*
* OpenAI Assistants APIのsubmit_tool_outputsを呼び出す。
* Ref: https://platform.openai.com/docs/api-reference/runs/submitToolOutputs
*/
l_url := l_url || '/submit_tool_outputs';
l_request_json := json_object_t();
l_request_json.put('tool_outputs', l_tool_outputs);
l_request := l_request_json.to_clob();
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('OpenAI-Beta','assistants=v1',p_reset => false);
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
,p_credential_static_id => p_credential_static_id
);
if apex_web_service.g_status_code <> 200 then
apex_debug.info('status = %s, reseponse = %s', apex_web_service.g_status_code, l_response);
raise e_api_call_failed;
end if;
p_response := l_response;
end submit_tool_outputs;
end utl_openai_tools_api;
/

アシスタントの作成時にツールとしてCode InterpreterFunction Callingを設定できるようにします。

ページ・デザイナでページ番号のAssistant Detailのフォームを開きます。

ページ・アイテムP5_TOOLSを選択します。現在はデフォルトの値を[{ "type": "retrieval" }]として、ツールとしてRetrievalを使用する設定になっています。

識別タイプチェック・ボックス・グループに変更します。設定列の数にします。LOVタイプ静的値を選択し、静的値として以下を設定します。戻り値CODE_INTERPRETERRETRIEVALFUNCTION_CALLINGのどれか、または全てを含みます。


追加値の表示オフです。

デフォルトタイプ- 選択 -に戻し、無指定にします。


ツールに含めるfunctionを決めるfunction setを選択するページ・アイテムを作成します。作成したページ・アイテムはページ・アイテムP5_FILE_IDSの下に配置します。

識別名前P5_FUNCTION_SET_IDタイプラジオ・グループです。レイアウト新規行の開始オフにし、ページ・アイテムP5_FILE_IDSの右隣に配置します。

LOVタイプ共有コンポーネントを選択し、LOVとしてLOV_FUNCTION_SETを選びます。追加値の表示オフNULL値の表示オフです。

セッション・ステートストレージリクエストごと(メモリーのみ)とし、サーバー側の条件タイプアイテムはNULLアイテムP5_IDを指定し、アシスタントの作成時のみ設定できるようにします。


プロセス・ビューを開き、プロセスCreate Assisantのパラメータp_toolsを選択します。

タイプファンクション本体に変更し、PL/SQLファンクション本体として以下を記述します。

declare
l_tools json_array_t;
l_code_interpreter boolean;
l_retrieval boolean;
l_function_set_id number;
begin
if instr(:P5_TOOLS,'CODE_INTERPRETER') > 0 then
l_code_interpreter := true;
end if;
if instr(:P5_TOOLS,'RETRIEVAL') > 0 then
l_retrieval := true;
end if;
if instr(:P5_TOOLS, 'FUNCTION_CALLING') > 0 then
l_function_set_id := :P5_FUNCTION_SET_ID;
end if;
return utl_openai_tools_api.generate_tools_json(
p_code_interpreter => l_code_interpreter
,p_retrieval => l_retrieval
,p_function_set_id => l_function_set_id
);
end;


以上で作成するアシスタントのツールにCode Interpreter、RetrievalおよびFunction Callingを含めることができるようになりました。

現在の天気を確認するアシスタントを作成します。NameMy Weather Forecast AssistantInstructionsは「あなたは日本の天気を教えてくれるアシスタントです。」とします。ToolsFunction Callingにチェックを入れ、Function SetとしてWeatherを選択します。

作成をクリックします。


APIを呼び出して作成したアシスタントも、OpenAIのAssistantsのPlaygroundで扱うことができます。


アシスタントを作成したので、Messagesのページよりスレッドの作成を行います。メッセージとして「現在の東京の天気を教えてください。」を作成し、スレッド実行します。

実行したスレッドはStatusrequires_actionで停止します。

アシスタントがファンクションの呼び出しを要求していて、その結果を待っている状態です。Oracle APEX側で要求されているストアド・プロシージャを実行し、その実行結果をOpenAI Assistants APIのSubmit Tool Outputsを呼び出してアップロードする必要があります。


Runオブジェクトの内容を表示するページを作成し、そのページにSubmit Tool Outputsを呼び出すボタンを作成します。

ページの作成をクリックし、ページ作成ウィザードを開始します。

フォームを選択します。


ページ番号8名前Run Detailとします。ページ・モードドロワーを選択します。データ・ソースとしてRESTデータ・ソースを選択し、RESTデータ・ソースとしてList runsを選びます。

ドロワーなので、ナビゲーションは双方ともオフです。

へ進み、主キー列1ID (Varchar2)を選択し、ページを作成します。


作成されたページを編集します。

ページ・アイテムP8_IDの除くすべてのページ・アイテムを選択し、識別タイプ表示のみに変更します。


デバッグ用にページ・アイテムP8_RESPONSEを作成します。タイプテキスト領域ラベルResponseとします。

セッション・ステートデータ型CLOBストレージリクエストごと(メモリのみ)を指定します。


レンダリング前ヘッダーの前にあるプロセス初期化フォームRun Detailを選択します。

識別タイプAPIの呼出しに変更し、設定パッケージとしてUTL_OPENAI_ASSISTANTS_APIプロシージャまたはファンクションとしてRETRIEVE_RUNを指定します。

サーバー側の条件タイプアイテムはNULLではないを選択し、アイテムとしてP8_IDを指定します。


パラメータp_run_idを選択し、アイテムとしてP8_IDを指定します。


パラメータファンクションの結果p_toolsp_file_idsp_metadataを選択し、パラメータ出力を無視オンにします。


Assistants APIのSubmit Tool Outputsを発行するボタンを作成します。

識別ボタン名SUBMIT_TOOL_OUTPUTSラベルSubmit Tool Outputsとします。外観ホットオンテンプレート・オプションWidthStretchにします。

動作アクションはデフォルトのページの送信です。

サーバー側の条件タイプアイテム = 値を選択し、アイテムとしてP8_STATUSrequires_actionを指定します。


プロセス・ビューを開き、Submit Tool Outputsを発行するプロセスを作成します。作成したプロセスはダイアログを閉じるの上に配置します。

識別名前Submit Tool OutputsタイプとしてAPIの呼出しを選択します。設定パッケージUTL_OPENAI_TOOLS_APIプロシージャまたはファンクションとしてSUBMIT_TOOL_OUTPUTSを指定します。

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


パラメータp_run_idの値のアイテムはP8_IDとします。


パラメータp_credential_static_idは、静的値&G_OPENAI_API_KEY.です。

パラメータp_required_actionp_tool_outputは、パラメータ出力を無視オンにします。


プロセスダイアログを閉じるを選択し、サーバー側の条件SUBMIT_TOOL_OUTPUTSを追記します。ボタンSUBMIT_TOOL_OUTPUTSを押した後にドロワーが閉じられます。


フォームのページについては、以上で完成です。

ページ番号List runsをソースとした対話モード・レポートから、ドロワーを開く設定を追加します。

リージョンRunsを選択し、プロパティ・エディタ属性タブを開きます。リンクターゲットをクリックし、リンク・ビルダーを開きます。


ターゲットタイプとしてこのアプリケーションのページページとして先ほど作成したドロワーのページである8を指定します。

アイテムの設定名前P8_ID(P7_IDから変更します)に\#ID#\、その次に名前P8_THREAD_ID\&P6_THREAD_ID.\を設定します。


リージョンRunsから開かれたドロワーが閉じた時に、対話モード・レポートがリフレッシュされるように、動的アクションをリージョンRunsに作成します。

作成した動的アクションの識別名前ダイアログのクローズとします。タイミングイベントとしてダイアログのクローズを選択し、選択タイプリージョンリージョンとしてRunsを指定します。


TRUEアクションとしてリフレッシュを選択します。影響を受ける要素選択タイプリージョンリージョンとしてRunsを選択します。


以上で、OpenAIのAssistants APIからCode InterpreterとFunction Callingを呼び出す機能を追加できました。

Code Interpreterの処理の過程を確認するためには、Run Stepsを見る必要があります。

Run Stepsを参照するページを追加します。

RESTソース・カタログのOpenAI Assistants APIよりList run stepsを選択し、RESTデータ・ソースList run stepsを作成します。


RESTデータ・ソースList run stepsをソースとしたフォーム付き対話モード・レポートのページを作成します。主キー列1ID (Varchar2)を選択します。


対話モード・レポートのページに、RESTソースList run stepsの必須パラメータであるRun IDを保持するページ・アイテムP13_RUN_IDを作成します。

タイプテキスト・フィールドラベルRun IDセッション・ステートストレージセッションごと(永続)とします。


同様に必須パラメータであるThread IDを保持するページ・アイテムP13_THREAD_IDを作成します。ラベルThread IDになります。


リージョンRun Stepsのパラメータを設定します。

パラメータrun_idタイプアイテムアイテムとしてP13_RUN_IDを指定します。


パラメータthread_idタイプアイテムアイテムとしてP13_THREAD_IDを指定します。


リージョンRun Stepsを選択し、ソース送信するページ・アイテムとしてP13_RUN_IDP13_THREAD_IDを指定します。


プロパティ・エディタ属性タブを開き、リンクターゲットをクリックしてリンク・ビルダーを開きます。


アイテムの設定に、名前P14_RUN_ID\#RUN_ID#\名前P14_THREAD_ID\#THREAD_ID#\の組み合わせを追加します。


ページ・デザイナでページ番号14のフォームのページを開きます。

Run Stepの詳細を確認するためにページ・アイテムを4つ作成します。タイプはすべて表示のみです。

作成するページ・アイテムはP14_RESPONSEP14_STEP_DETAILSP14_EXPIRE_ATP14_METADATAです。セッション・ステートデータ型P14_EXPIRED_ATを除いてCLOBP14_EXPIRED_ATVARCHAR2)、ストレージリクエストごと(メモリーのみ)とします。


ページ・アイテムP14_IDを除いた既存のページ・アイテムをすべて選択し、タイプ表示のみに変更します。


レンダリング前ヘッダーの前にあるプロセス初期化フォームRun Stepsを選択します。

識別タイプAPIの呼出しに変更し、設定パッケージとしてUTL_OPENAI_ASSISTANTS_APIプロシージャまたはファンクションとしてRETRIEVE_RUN_STEPを指定します。

サーバー側の条件タイプアイテムはNULLではないを選択し、アイテムとしてP14_IDを指定します。


パラメータp_step_idタイプアイテムアイテムP14_IDとします。


パラメータファンクションの結果パラメータ出力を無視オンにします。


ページ番号6のMessagesの対話モード・レポートからRun Stepsのページに遷移できるように、リンクを設定します。

リージョンMessagesの列RUN_IDを選択し、タイプリンクに変更します。リンクターゲットクリックし、リンク・ビルダーを開きます。


ターゲットタイプこのアプリケーションのページページ13を指定します。

アイテム設定に、名前P13_RUN_ID#RUN_ID#名前P13_THREAD_ID#THREAD_ID#の組み合わせを追加します。


以上でRun Stepsを表示するページも完成です。

以下はCode Interpreterを呼び出したGIF動画になります。


今回の記事は以上になります。

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