2025年3月25日火曜日

OpenAI Responses APIをOracle APEXのアプリケーションから呼び出す

新たにOpenAIから提供されたResponses APIを、Oracle APEXのアプリケーションから(正確にいうとOracle Databaseから)呼び出してみます。OpenAIからは2025年3月11日のニュース「エージェント開発のための新たなツール」にて、Responses APIについて紹介されています。

簡単なAPEXアプリケーションを作成し、Responses APIの組み込みツールのWeb検索previous_response_idを指定した会話の継続、およびReasoningモデルの呼び出しを確認してみます。

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


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

機能はすべてホーム・ページに実装しています。

モデルを選択するページ・アイテムとしてP1_MODELを作成しています。タイプ選択リストです。


選択リストの戻り値を、APIリクエストのパラメータmodelに与えています。


会話を継続するためのパラメータprevious_response_idとして与えるIDを保存するページ・アイテムとしてP1_RESPONSE_IDを作成しています。Responses APIを呼び出した後に、レスポンスに含まれるidを取り出しP1_RESPONSE_IDに設定します。

OpenAIのAPI Referenceによると、"Even when using previous_response_id, all previous input tokens for responses in the chain are billed as input tokens in the API.”とのことなので、input配列にメッセージ履歴を含めるのと、費用面では変わらないようです。


パラメータinstructionsとして指定する文字列をページ・アイテムP1_INSTRUCTIONSに指定します。実際にはinstructionsの代わりに、roledeveloperとしたcontentとしてinput配列に含めています。

指定がなければAPIリクエストには含めません。


ユーザーによるプロンプトをページ・アイテムP1_INPUTに入力します。タイプテキスト領域です。roleusercontentになります。


ボタンSUBMITをクリックすると、Responses APIを呼び出します。


ボタンSUBMITをクリックしたときに、以下のPL/SQLプロシージャOPENAI_RESPONSES_APIが実行されるようにします。

create or replace procedure openai_responses_api(
p_model in varchar2
,p_input in varchar2
,p_instructions in varchar2
,p_response_id in out varchar2
,p_output_text out clob
,p_annotations out clob
,p_usage out clob
,p_endpoint in varchar2 default 'https://api.openai.com/v1/responses'
,p_credential_static_id in varchar2 default 'OPENAI_API_KEY'
)
as
/* リクエストの作成に使用する */
l_request clob;
l_request_json json_object_t;
l_tools json_array_t;
l_tool json_object_t;
l_reasoning json_object_t;
l_input json_array_t;
l_input_obj json_object_t;
e_openai_api_call_failed exception;
/* レスポンスの処理に使用する。 */
l_response clob;
l_response_json json_object_t;
l_object varchar2(40);
l_status varchar2(20); /* completed, failed, in_progress, incomplete */
l_output json_array_t;
l_output_size integer;
l_output_obj json_object_t;
l_output_obj_type varchar2(40);
l_output_obj_status varchar2(40);
l_output_obj_role varchar2(40);
l_content json_array_t;
l_content_size integer;
l_content_obj json_object_t;
l_content_obj_type varchar2(20);
-- l_content_obj_text clob;
/*
* typeがoutput_textのcontent
*/
l_output_text clob;
l_annotations json_array_t;
/*
* usage
*/
l_usage json_object_t;
begin
l_request_json := json_object_t();
/* model指定
* https://platform.openai.com/docs/api-reference/responses/create#responses-create-model
*/
l_request_json.put('model', p_model);
/*
* reponse idが与えられていれば、会話を継続させる。
* https://platform.openai.com/docs/api-reference/responses/create#responses-create-previous_response_id
*/
if p_response_id is not null then
l_request_json.put('previous_response_id', p_response_id);
end if;
/*
* GPTモデルであれば、Web検索をtoolに含める
* https://platform.openai.com/docs/api-reference/responses/create#responses-create-tools
*/
if p_model like 'gpt%' then
l_tools := json_array_t();
l_tool := json_object_t();
l_tool.put('type', 'web_search_preview');
l_tools.append(l_tool);
l_request_json.put('tools', l_tools);
end if;
/*
* Reasoningモデルであれば、effortをmediumにする。
* https://platform.openai.com/docs/api-reference/responses/create#responses-create-reasoning
*/
if p_model like 'o3%' then
l_reasoning := json_object_t();
l_reasoning.put('effort', 'medium');
if p_model like 'o1%' then
l_reasoning.put('generate_summary', 'concise');
end if;
l_request_json.put('reasoning', l_reasoning);
end if;
/*
* ユーザー・プロンプトの指定
* https://platform.openai.com/docs/api-reference/responses/create#responses-create-input
*/
l_input := json_array_t();
l_input_obj := json_object_t();
/*
* p_instructionsの指定があれば、developerロールのメッセージとして追加。
*/
if p_instructions is not null then
l_input_obj.put('role', 'developer');
l_input_obj.put('content', p_instructions);
l_input.append(l_input_obj);
l_input_obj := json_object_t();
end if;
/*
* ユーザー・ロールのメッセージを追加
*/
l_input_obj.put('role', 'user');
l_input_obj.put('content', p_input);
l_input.append(l_input_obj);
l_request_json.put('input', l_input);
/* 送信するメッセージ */
l_request := l_request_json.to_clob();
apex_debug.info('request = %s', l_request);
/* OpenAIのResponses APIの呼び出し。 */
apex_web_service.set_request_headers('Content-Type', 'application/json');
l_response := apex_web_service.make_rest_request(
p_url => p_endpoint
,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
raise e_openai_api_call_failed;
end if;
apex_debug.info('response = %s', l_response);
l_response_json := json_object_t(l_response);
p_response_id := l_response_json.get_string('id');
l_object := l_response_json.get_string('object');
l_status := l_response_json.get_string('status');
l_usage := l_response_json.get_object('usage');
/* outputの取り出し */
l_output := l_response_json.get_array('output');
apex_debug.info('output = %s', l_output.to_string());
l_output_size := l_output.get_size();
apex_debug.info('output length = %s', l_output_size);
l_output_text := '';
l_annotations := json_array_t();
for i in 1..l_output_size
loop
l_output_obj := treat(l_output.get(i-1) as json_object_t);
l_output_obj_type := l_output_obj.get_string('type');
l_output_obj_status := l_output_obj.get_string('status');
/*
* output_extはtypeがmessageのオブジェクトより取り出す。
*/
if l_output_obj_type = 'message' then
l_output_obj_role := l_output_obj.get_string('role'); /* assistantなはず */
/* contentの取り出し */
l_content := l_output_obj.get_array('content');
apex_debug.info('content = %s', l_content.to_string());
l_content_size := l_content.get_size();
apex_debug.info('content length = %s', l_content_size);
for j in 1..l_content_size
loop
l_content_obj := treat(l_content.get(j-1) as json_object_t);
l_content_obj_type := l_content_obj.get_string('type');
if l_content_obj_type = 'output_text' then
l_output_text := l_output_text || l_content_obj.get_string('text');
apex_debug.info('output_text = %s', l_output_text);
l_annotations.append_all(l_content_obj.get_array('annotations'));
end if;
end loop;
end if;
end loop;
p_output_text := l_output_text;
p_annotations := l_annotations.to_clob();
p_usage := l_usage.to_string();
end openai_responses_api;
/

識別タイプとしてAPIの呼出しを選択し、設定タイプPL/SQLプロシージャまたはファンクションを選択し、プロシージャまたはファンクションOPENAI_RESPONSES_APIとします。


ボタンCLEARをクリックすると、タイプセッション・ステートのクリアであるプロセスを呼び出します。


APIレスポンスからはマークダウンが返されることを想定しています。レスポンスを表示するページ・アイテムP1_OUTPUT_TEXTタイプMarkdownエディタ読取り専用常時を設定します。


JSON配列として返されるannotationsクラシック・レポートに表示します。annotations自体は非表示のページ・アイテムP1_ANNOTATIONSに設定します。

ソースのSQL問合せに以下を記述します。
select j.type, j.title, j.url, j.start_index, j.end_index
from json_table(:P1_ANNOTATIONS, '$[*]'
    columns
    (
        type        varchar2(20)  path '$.type',
        title       varchar2(400) path '$.title',
        url         varchar2(200) path '$.url',
        start_index number        path '$.start_index',
        end_index   number        path '$.end_index'
    )
) j
サーバー側の条件を設定し、annotationsが返されているときだけクラシック・レポートを表示するようにします。
declare
    l_array json_array_t;
begin
    if :P1_ANNOTATIONS is not null then
        l_array := json_array_t(:P1_ANNOTATIONS);
        if l_array.get_size() > 0 then
            return true;
        end if;
    end if;
    return false;
end;

usageはJSONをそのままページ・アイテムP1_USAGEに表示します。


作成したAPEXアプリケーションの説明は以上です。

作成したAPEXアプリケーションを使って、OpenAIのResponses APIを呼び出してみます。

最初にモデルgpt-4o-miniを選択し、inputとして以下を入力します。

「これから開かれる大阪万博の期間を教えて。」

toolsとして組み込みのweb_search_previewを渡しているため、引用先も含めた回答が得られています。APIレスポンスに含まれるidがResponse Idに設定されます。


続けて以下を問合せます。Response Idが指定されているため、大阪万博に関する会話が継続します。

「日本からの展示にはどのようなものがありますか?」


会話を継続します。今度はinstructionsに「英語で答えてください。」を指定した上で、以下を問い合わせます。

「海外からのパビリオンではどのようなものがありますか?」

回答が日本語になったり、引用が無かったりしました。スイスとドイツは確認したところ、情報としては正しそうです。



モデルとしてo3-miniを選択し、以下を問い合わせました。

「万国博覧会の意義を教えて。」


Responses APIがChat Completions APIのように他の実装(vLLM、Ollamaやllama.cppといったところ)で採用されるかどうかは分かりませんが、Assistants APIよりは採用するハードルは低そうです。

とりあえず、previous_response_idの指定だけで会話を継続できるのは、大変便利です。

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

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