今までローカル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がインラインで記述されたファンクションを、動的に作成しています。
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_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コードに以下を記述します。
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_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コードに置き換えてみます。
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
/* | |
* 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] + "¤t=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のファンクションと全く同じです。
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_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; |
動作が確認できました。
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のアプリケーション作成の参考になれば幸いです。
完