2024年4月26日金曜日

LLMのツール呼び出しで呼び出されるファンクションをJavaScriptで記述する

Oracle Database 23cよりMLE(MultiLingual Engine - GraalVMのこと)が組み込まれ、データベースでJavaScriptを実行することができます。

今までローカルLLMを呼び出す際に使用しているデータベースがOracle Database 23c Freeなので、JavaScriptを使うことができます。LLMを扱うような開発者がPL/SQLでコードを書くということはあまり無さそうなので、ツール呼び出しで呼び出されるファンクションをJavaScriptで書けるようにしてみました。

以下のようにツールを登録する際に、JavaScriptのファンクション本体を記述するページ・アイテムを追加します。


ツールの情報を入力するフォームにページ・アイテムP3_JAVASCRIPT_FUNCTION_BODYを作成します。

タイプテキスト領域ラベルJavaScript Function Bodyとします。

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


ツールの作成または変更の適用を実行したときに、本体がJavaScriptで記述されたファンクションの作成または更新を行います。

プロセスを新規に作成します。

識別名前Create or Replace JavaScript Functionタイプコードを実行を選択します。ソースPL/SQLコードには以下を記述します。ファンクションの本体としてJavaScriptがインラインで記述されたファンクションを、動的に作成しています。

declare
l_ddl clob;
begin
/*
* 引数pArgsのタイプとしてCLOBを指定すると値が正しく渡らない。JSONにするべきかもしれないが、PL/SQLとの
* 互換性を考慮して - 23c以前のDBでもこのアプリは動く - VARCHAR2とする。
*/
l_ddl := 'create or replace function ' || :P3_TOOL_NAME || '('
|| '"pArgs" in varchar2) return clob as mle language javascript q' || chr(39) || chr(126)
|| :P3_JAVASCRIPT_FUNCTION_BODY || chr(126) || chr(39) || ';';
apex_debug.info(l_ddl);
execute immediate l_ddl;
end;

サーバー側の条件タイプを選び、PL/SQL式として以下を記述します。ボタン作成または変更の適用がクリックされたときに、JavaScript Function Bodyが空でなければファンクションの作成または更新を行います。
:REQUEST in ('SAVE', 'CREATE') and :P3_JAVASCRIPT_FUNCTION_BODY is not null

フォームが開く時にページ・アイテムP3_JAVASCRIPT_FUNCTION_BODYを初期化するプロセスを作成します。

レンダリング前ヘッダーの前初期化フォームTool Detailの下にプロセスを作成します。

識別名前Load JavaScript Sourceタイプコードを実行を選択します。ソースPL/SQLコードに以下を記述します。

declare
l_text varchar2(32767);
l_clob clob;
l_start integer;
l_end integer;
begin
dbms_lob.createTemporary(
lob_loc => l_clob
,cache => false
,dur => DBMS_LOB.CALL
);
for r in (
select text from user_source where name = upper(:P3_TOOL_NAME) order by to_number(line) asc
)
loop
dbms_lob.writeAppend(
lob_loc => l_clob
,amount => length(r.text)
,buffer => r.text
);
end loop;
-- dbms_output.put_line(l_clob);
l_start := dbms_lob.instr(
lob_loc => l_clob
,pattern => 'q''~'
);
l_clob := dbms_lob.substr(
lob_loc => l_clob
,offset => (l_start + 3)
);
l_clob := dbms_lob.substr(
lob_loc => l_clob
,amount => length(l_clob) - 3
);
-- dbms_output.put_line(l_clob);
:P3_JAVASCRIPT_FUNCTION_BODY := l_clob;
dbms_lob.freeTemporary(
lob_loc => l_clob
);
end;

JavaScriptのファンクションが作成されているときにソースコードを取得するように、サーバー側の条件を設定します。

タイプ行が返されるを選択し、SQL問合せとして以下を記述します。
select 1 from user_mle_procedures where object_name = upper(:P3_TOOL_NAME)

以上でアプリケーションの変更は完了です。

ファンクションGET_CURRENT_WEATHERの本体を以下のJavaScriptコードに置き換えてみます。

/*
* fetchとoracledbのモジュールを使う。
*/
await import("mle-js-fetch");
const oracledb = require("mle-js-oracledb");
const conn = oracledb.defaultConnection();
/* 入力パラメータはJSONだが文字列として渡される */
const lArgs = JSON.parse(pArgs);
const location = lArgs.location.split(",")[0];
console.log("location = ", location);
/* 首都の名前から座標を取得する */
let sql = "select lat, lon from amano_city_locations where upper(capital_en) = upper(:location) or capital_jp = :location";
const coordinates = conn.execute(sql, [location, location]);
console.log(JSON.stringify(coordinates.rows[0]));
/* OpenMeteo.comに現在の天候を問い合わせる */
const url = "https://api.open-meteo.com/v1/forecast?latitude=" + coordinates.rows[0][0] + "&longitude=" + coordinates.rows[0][1] + "&current=weathercode";
console.log(url);
const response = await fetch(url);
const data = await response.json();
console.log(JSON.stringify(data));
/* 天候コードを取り出す */
const weathercode = Number(data.current.weathercode);
console.log(weathercode);
if ( weathercode < 10) {
// 10以下はWMO4501として扱う。
sql = "select description from wmo4501 where code = :weathercode";
} else {
// それ以外はWMO4677mとして扱う。
sql = "select description from wmo4677 where code = :weathercode";
}
/* 検索された天候の説明を戻り値とする */
const weather = conn.execute(sql, [weathercode]);
console.log(JSON.stringify(weather));
/* 戻り値は文字列 */
const ret = JSON.stringify({ "weather" : weather.rows[0][0] });
return ret;

処理の内容はPL/SQLのファンクションと全く同じです。


SQLコマンドより、作成されたJavaScriptのファンクションを呼び出してみます。以下のコマンドを実行します。

declare
l_args clob;
l_ret clob;
begin
DBMS_MLE.set_stdout_to_dbms_output();
l_args := '{ "location": "Tokyo" }';
l_ret := get_current_weather(l_args);
dbms_output.put_line(l_ret);
end;
view raw test_code.sql hosted with ❤ by GitHub


動作が確認できました。

RESTサービスの呼び出し時にACLのエラーが発生することがあります。

PL/SQLの場合は一般的にAPEX_WEB_SERVICEパッケージを使ってRESTサービスを呼び出します。このときはAPEXスキーマからUTL_HTTPパッケージが呼び出されるため、ACLはAPEXスキーマをプリンシパルとして許可が与えられています。今回の処理はAPEXのワークスペース・スキーマがUTL_HTTPを呼び出しているため、プリンシパルをAPEXのワークスペース・スキーマとしてACLを追加する必要があります。

以下のようにして、DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACEを呼び出します。principal_nameとしてワークスペース・スキーマ名を指定します。
begin
  DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(
    host => '*',
    ace => xs$ace_type(
        privilege_list => xs$name_list('connect'),
        principal_name => 'APEXDEV',
        principal_type => xs_acl.ptype_db));
  commit; 
end;
/
今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/chat-with-generative-ai-fc-js.zip

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