2023年11月7日火曜日

OpenAI ChatGPTのFunction Callingを使ってPL/SQLプロシージャを呼び出す

新しい記事で更新されています。

OpenAI Chat Completions APIのツール呼び出しを使ってPL/SQLファンクションを呼び出す
https://apexugj.blogspot.com/2024/04/chat-with-generative-ai-fc-sample-app.html

以下は過去の内容です。

以前にOpenAIのChatGPTのAPIを呼び出すサンプル・アプリケーションを作成しました。そのときの記事は「OpenAIのChatGPTのAPIを呼び出すAPEXアプリを作る」です。

今回は、以前の記事で作成したAPEXアプリケーションにFunction Callingを扱うコードを追加してみました。

OpenAIのブログ記事「Function calling and other API updates」を元にサンプルを実装しています。この記事では、都市を指定して現在の天気を質問しています。OpenAIの記事での質問は「What is the weather like in Boston?」ですが、今回作成したサンプル・アプリケーションには、「現在の東京の天気を教えてください。」と質問しています。

最初に質問を投げたときは、Function Set(これはOpenAIのAPIの引数ではなく、今回のサンプル・アプリケーションで作った設定です)の指定が無いため、API呼び出しに属性functionsが含まれず、ChatGPTからは「申し訳ありませんが、私は情報を提供することができません。」と返されています。2回目はFunction Callingが行われて外部の天気APIサービス(Open-Meteo.comを呼び出しています)より現在の東京の天気を取得しているため、「現在の東京の天気は「晴れ」です。」と回答されています。


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

以下より追加した実装について紹介します。

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

クイック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には、ファンクションの名前(APIでは属性name)、ファンクションの説明(属性description)およびファンクションのパラメータ定義(属性parameters)を定義します。手っ取り早く開発するため、parametersにはJSON形式のドキュメントをそのまま記述します。

OPENAI_FUNCTION_SETでは、表OPENAI_FUNCTIOINSに定義したファンクションをひとまとめにします。ChatGPTのAPIを呼び出す際に、ここで設定したファンクションセット名を指定することで、そのセットに含まれるファンションをAPIの属性functionsに含めます。

今回は表OPENAI_FUNCTIONSおよびOPENAI_FUNCTION_SETにデータが投入できるアプリケーションがあればよいので、クイックSQLによる表の生成からアプリケーションの作成までを、続けて実施します。


生成されたSQLスクリプトを即時実行した後、アプリケーションの作成を開始します。次に開く確認画面でも、アプリケーションの作成をクリックします。


アプリケーション作成ウィザードが起動します。表OPENAI_FUNCTIONSOPENAI_FUNCTION_SETレポートとフォームのページが追加済みになっています。

アプリケーションの名前OpenAI Function Callingとし、アプリケーションの作成を実行します。


アプリケーションが作成されます。

アプリケーションを実行し、ChatGPTから呼び出すファンクションを設定します。


OpenAIのブログ記事に従って、ファンクションget_current_weatherを定義します。

Functionsを開きます。


作成をクリックします。


Function nameとしてget_current_weatherDescriptionとしてGet the current weather in a given locationParametersには以下のJSONドキュメントを記述します。
{
  "type": "object",
  "properties": {
    "location": {
      "type": "string",
      "description": "The city and state, e.g. San Francisco, CA"
    },
    "unit": {
      "type": "string",
      "enum": [
        "celsius",
        "fahrenheit"
      ]
    }
  },
  "required": [
    "location"
  ]
}

ファンクションget_current_weatherが登録されました。このget_current_weatherは必ずPL/SQLのファンクションとして、CLOBの引数が1つ(JSONが渡されることを想定)、戻り値はCLOB(こちらもJSONが返されることを想定)であることを前提としています。

ファンクションget_current_weatherのコードは以下です。

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;
/
都市名から緯度経度を求めるために、アマノ技研様が公開している「世界の百万都市の位置データ Location Data of Megacities」のCSVデータをダウンロードし、表AMANO_CITY_LOCATIONS(新規作成)にロードしています。データのロードには、SQLワークショップデータ・ワークショップを使用しています。

また、weathercodeについては「気象関連コード表」に記載されているWMO 4501のHTML表をデータ・ワークショップにコピペし、表WMO4501(新規作成)にロードしています。

ファンクションの登録は以上です。

Function Setのページを開き、作成をクリックします。


Functionとして先ほど作成したget_current_weatherを選択します。Function Set NameWeatherとします。

作成をクリックします。


ファンクション・セットWeatherが登録されました。


以前に作成したChatGPTのAPIを呼び出すアプリケーションに機能を追加します。

最初にプロシージャcall_openai_apiを作成します。以前は動的アクションサーバー側のコードを実行にコードを直書きしていましたが、コードが長くなりすぎ制限を超えてしまいました。

create or replace procedure call_openai_api(
p_collection_name in varchar2 default 'CHATGPT'
,p_user_message in varchar2
,p_model_name in varchar2 default 'gpt-3.5-turbo-0613'
,p_function_set_id in number default null
,p_credential_static_id in varchar2 default 'OPENAI_API_KEY'
,p_request_out out varchar2
,p_response_out out varchar2
)
as
l_request json_object_t;
l_request_clob clob;
l_messages json_array_t;
l_message json_object_t;
/* function callingの対応 */
l_functions json_array_t;
l_function json_object_t;
l_parameters json_object_t;
l_function_call json_object_t;
l_finish_reason varchar2(4000);
l_function_name varchar2(200);
l_function_args clob;
l_dynamic_sql varchar2(4000);
l_function_out clob;
/*
  * OpenAIのドキュメントより、APIの応答例を参照。
* 参考: https://platform.openai.com/docs/guides/chat
*/
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;
e_openai_api_exception exception;
begin
/*
  * ユーザーによる直近のメッセージをコレクションに追記する。
*/
apex_collection.add_member(
p_collection_name => p_collection_name
,p_c001 => 'user'
,p_clob001 => p_user_message
);
/*
  * APEXコレクションより、作成時刻の昇順でメッセージの配列にする。
*/
l_messages := json_array_t();
for r in (select c001, clob001 from apex_collections where collection_name = p_collection_name order by seq_id)
loop
l_message := json_object_t();
l_message.put('role' ,r.c001);
l_message.put('content',r.clob001);
l_messages.append(l_message);
end loop;
l_request := json_object_t();
l_request.put('model',p_model_name);
l_request.put('messages', l_messages);
/*
* Function Setが指定されていれば、functionの配列を
* リクエストに含める。
*/
if p_function_set_id is not null then
l_functions := json_array_t();
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_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_request.put('functions', l_functions);
end if;
/*
* temprature, top_pなどのパラメータを設定するとしたら、ここでputする。
*/
l_request_clob := l_request.to_clob();
p_request_out := l_request_clob;
/*
* OpenAIのChatGPTの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 => 'https://api.openai.com/v1/chat/completions'
,p_http_method => 'POST'
,p_body => l_request_clob
,p_credential_static_id => p_credential_static_id
);
p_response_out := l_response_clob;
if apex_web_service.g_status_code <> 200 then
apex_debug.info(l_response_clob);
raise e_openai_api_exception;
end if;
/* ドキュメントに記載されているレスポンスで処理を継続する。 */
l_response := json_object_t(l_response_clob);
l_choices := l_response.get_array('choices');
l_choice0 := treat(l_choices.get(0) as json_object_t);
/* finish_reaonがfunction_callで終わっているか、後で確認する。 */
l_finish_reason := l_choice0.get_string('finish_reason');
l_message := l_choice0.get_object('message');
l_role := l_message.get_string('role');
l_content := l_message.get_clob('content');
/* finish_reasonがfunction_callであれば、指定されたファンクションを呼び出す。 */
if l_finish_reason = 'function_call' then
l_function_call := l_message.get_object('function_call');
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');
/* ストアド・プロシージャを動的に呼び出す。 */
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_content := l_function_out;
end if;
end if;
/* usageも取り出す。 */
l_usage := l_response.get_object('usage');
/*
* ChatGPTからの応答をAPEXコレクションに追記する。
*/
apex_collection.add_member(
p_collection_name => p_collection_name
,p_c001 => l_role
,p_clob001 => l_content
,p_n001 => l_usage.get_number('prompt_tokens')
,p_n002 => l_usage.get_number('completion_tokens')
,p_n003 => l_usage.get_number('total_tokens')
);
end call_openai_api;
/
送信するアイテムは、P1_USER_MESSAGEP1_FUNCTION_SETです。戻すアイテムP1_REQUESTP1_RESPONSEです。戻すアイテムは両方ともデバッグ用途で、無くても問題ありません。


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

識別名前P1_FUNCTION_SETタイプ選択リストです。ラベルFunction Setとします。LOVタイプSQL問合せを選択し、SQL問合せとして以下を記述します。

select function_set_name d, id r from openai_function_set
追加値の表示オフNULL値の表示オンとして、NULL表示値-- No function set assigned --を記述します。


デバッグ用にタイプ表示のみのページ・アイテムP1_REQUESTおよびP1_RESPONSEを作成します。


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

今回の記事は以上です。

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