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がインラインで記述されたファンクションを、動的に作成しています。


サーバー側の条件タイプを選び、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コードに以下を記述します。


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

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

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

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


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


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



動作が確認できました。

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のアプリケーション作成の参考になれば幸いです。

Oracle APEXのアプリからいろいろなローカルLLMを呼び出してみる

Apple MシリーズのDocker/Colima上にOracle APEXが動作する環境を作成し(こちらの記事)、Ollamaを使って動かしたローカルLLMを呼び出すAPEXアプリケーションも作成しました(こちらの記事)。

この環境を使って、最近話題のMicrosoftのphi3(Mini, 3.8B, 4bit)、Googleのgemma(7B, 4bit)、Metaのllama3(8B, 4-bit)を動かして、APEXアプリケーションからOpenAI互換のChat Completions APIで呼び出してみます。

呼び出すモデルを変更する度に、アプリケーション・ビルダーを呼び出して置換文字列を変更するのは面倒なので、APIのエンドポイントモデル名Web資格証明をアプリケーションから変更できるように、ページ・アイテムを追加します。

ページ・アイテムP1_TOOL_SETの下に、APIエンドポイントを設定するページ・アイテムP1_API_ENDPOINTを作成します。タイプテキスト・フィールドラベルAPI Endpointとします。

レイアウト新規行の開始オン検証必須の値オンデフォルトタイプ静的を選択し、静的値として&G_API_ENDPOINT.を指定します。

セッション・ステートストレージセッションごと(永続)を選択します。


モデル名を設定するページ・アイテムP1_MODEL_NAMEを作成します。タイプテキスト・フィールドラベルModel Nameとします。

レイアウト新規行の開始オフとしてページ・アイテムP1_API_ENDPOINTの右に配置します。検証必須の値オンデフォルトタイプ静的を選択し、静的値として&G_MODEL_NAME.を指定します。

セッション・ステートストレージセッションごと(永続)を選択します。


Web資格証明の静的IDを設定するページ・アイテムP1_CREDENTIAL_STATIC_IDを作成します。タイプテキスト・フィールドラベルCredential Static IDとします。

レイアウト新規行の開始オフとしてページ・アイテムP1_MODEL_NAMEの右に配置します。検証必須の値オフ(ローカルLLMでは指定不要)、デフォルトタイプ静的を選択し、静的値として&G_CREDENTIAL_STATIC_ID.を指定します。

セッション・ステートストレージセッションごと(永続)を選択します。


カード・リージョンChat HistoryソースSQL問合せを以下に変更し、送受信したメッセージの文字数を検索結果に含めます。

select seq_id, c001, c002, clob001, n001, n002, n003, dbms_lob.getlength(clob001) cnt
from apex_collections
where collection_name = :G_CHAT_HISTORY order by seq_id desc


属性2次本体HTML式を以下に変更し、メッセージの文字数を表示します。
char count: &CNT.
{if N001/}
, prompt_tokens: &N001. completion_tokens: &N002. total_tokens: &N003.
{endif/}


プロセスSend Messageを選択し、パラメータの設定を変更します。

パラメータp_api_endpointタイプアイテムに変更し、アイテムとしてP1_API_ENDPOINTを指定します。


パラメータp_model_nameも同様に、アイテムとしてP1_MODEL_NAMEを指定します。


パラメータp_credential_static_idアイテムとしてP1_CREDENTIAL_STATIC_IDを指定します。


以上でAPEXアプリケーションから呼び出すモデルを変更できるようになりました。

最初にMicrosoftのphi3を呼び出してみます。

% ollama run phi3  

>>> Send a message (/? for help)


API Endpointhttp://host.docker.internal:11434/v1/chat/completionsModel Namephi3です。

以下のメッセージを送信しました。

織田信長が活躍した時代は、一般に何時代と呼ばれていますか?


同様にGoogle gemmaを呼び出してみます。

% ollama run gemma

>>> Send a message (/? for help)


Model Namegemmaを指定します。


Metaのllama3を呼び出してみます。

% ollama run llama3

>>> Send a message (/? for help)


Model Namellama3を指定します。

英語で回答されました。


プロンプトとして「日本語で回答してください。」を設定し、同じ質問をしてみました。


OpenAIのgpt-4-turboを呼び出してみました。


Cohereのcommand-r-plus:104b-q2_Kを呼び出してみました。回答はあっさり「戦国時代」です。


もう少し詳しく回答してもらうために、プロンプトとして「あなたは日本史の先生です。」を設定し、同じ質問をしてみました。


OpenAI Chat Completions APIを呼び出す形でアプリケーションを作成することにより、本家OpenAIとOllamaのローカルLLMの切り替えが容易にできるようになりました。

今回機能を追加したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/chat-with-generative-ai-hc.zip

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

2024年4月25日木曜日

Ollama + command-r-plus:104b-q2_KのローカルLLMでファンクション呼び出しを試す

以前の記事「OpenAI Chat Completions APIのツール呼び出しを使ってPL/SQLファンクションを呼び出す」で作成したAPEXアプリケーションを使って、Ollama + command-r-plus:104b-q2_Kでファンクション呼び出しを試してみます。

Ollama自体はファンクション呼び出しをサポートしていないはずなのですが、プロンプトを調整すればそれっぽいことができそう、とのことで試してみました。以下の記事を参考にしています。

Function calling using Ollama models
https://www.deskriders.dev/posts/1702742595-function-calling-ollama-models/

結論から先に言うと、プロンプトの記述だけでは厳しいかな、と感じました。以下、行った作業を記述します。

APEXアプリケーション側の設定として、置換文字列のG_API_ENDPOINThttp://host.docker.internal:11434/v1/chat/completionsG_MODEL_NAMEにはcommand-r-plus:104b-q2_Kを設定しています。

ollamaをローカルで実行し、APEXアプリケーションにアクセスします。

Promptとして以下を与え、Set Promptをクリックします。ツール定義の部分はOpenAIのtoolの定義をそのまま埋め込んでいます。また出力の形式指定もOpenAIの仕様で返すように要求しています。



MessageとしてWhat is the current weather in Tokyo?を入力し、Send Messageをクリックします。


プロンプトが想定どおり解釈されると、roleがassistantのメッセージとして以下のようなJSONのレスポンスが返されます。

```json { "tool_calls": [ { "type": "function", "function": { "id": "get_current_weather_0", "name": "get_current_weather", "arguments": { "location": "Tokyo" } } } ] }```

パッケージUTL_OPENAI_CHAT_APIには、OpenAI以外のエンドポイントから上記ようなJSONが返されたときは、ファンクション呼び出しと解釈してPL/SQLファンクションを呼び出すようにコーディングしています。

そのため、ファンクションget_current_weatherが引数{ "location": "Tokyo"}で呼び出されます。ファンクション呼び出しのレスポンスは(OpenAI以外ではroleがtoolのメッセージに対応していないため)、roleをassistantにしてチャット履歴に追記します。

{ "weather" : "Clear(No cloud at any level)" }



JSONで天候の回答は得ていますが、これから追加でメッセージを送信するにはsystemプロンプトからファンクション呼び出しの設定を除く必要があります。

Promptを空にしてSet Promptをクリックします。 ロールがsystemのメッセージが無くなります。


Message日本語で回答してください。と入力し、Send Messageをクリックします。


以下の回答が返されました。

東京の現在の天気は晴れです。(高層雲なし)気温は 13°C で、南からの風が吹いています。


東京の天気について聞いているということは理解されていて、天気が晴れで雲がないところまでは合っていますが、気温や南からの風という情報は適当です。

systemプロンプトの記述によりOpenAIのtool_callsのJSONが返されるのは少々驚きましたが、これから先には進めないように感じました。

今回の記事は以上です。何かの参考になれば幸いです。

OpenAI Chat Completions APIのツール呼び出しを使ってPL/SQLファンクションを呼び出す

こちらの記事「OpenAIのChat Completions APIを呼び出すAPEXアプリを作成する」で作成したAPEXアプリケーションに、ツール呼び出し(以前のFunction Calling)の機能を追加します。このアプリケーションが呼び出しているパッケージUTL_OPENAI_CHAT_APIには、すでにツール呼び出しをハンドリングするコードが含まれているため、主な作業はユーザー・インターフェースの追加になります。

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

プロンプトとして「あなたはお天気お兄さんです。」を設定し、メッセージ「現在の東京とパリの天候を教えてください。」を送信します。

LLMはtool_callsとして引数をTokyoとしたツール呼び出しと、Parisを引数としたツール呼び出しを返します。ツールの並列呼び出しは2023年11月に追加された機能です。

TokyoとParisの現在の天候をOpenMeteo.comに問合せて回答を返し、最終的に「現在の東京とパリの天候はどちらも晴れです。」という回答を得ています。

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

はじめに呼び出すツールに関する設定を保存する表OPENAI_TOOLSを操作するために、対話モード・レポートとフォームのページを作成します。

ページの作成をクリックします。


対話モード・レポートを選択します。


対話モード・レポートのページの名前Toolsとし、フォーム・ページを含めるオンにします。フォーム・ページ名Tool Detailとします。

データ・ソース表/ビューの名前OPENAI_TOOLSを選択します。

へ進みます。


主キー列1ID(Number)を選択します。

ページの作成をクリックします。


以上でツールの設定画面が作成できました。

ツール呼び出しに登録するツールは、PL/SQLのストアド・ファンクションとして作成します。OpenAIではLLMのレスポンスとしてツール呼び出し(roletool_calls)を返す時は、呼び出すファンクション名とその引数となるJSONドキュメントをレスポンスに含めます。

レスポンスとしてツール呼び出しを受け取った場合は、ファンクション名として返されたPL/SQLファンクションにJSONドキュメントを引数として渡して、動的にファンクションを呼び出します。呼び出したファンクションの戻り値(これもJSONドキュメント)をツールの応答(roletool)としてLLMに戻します。

ページを実行し、指定した都市(首都)の天候を取得するファンクションget_current_weatherを設定します。

作成をクリックします。


Tool SetWeatherとします。Tool Typefunctionを指定します。現時点で指定できるTool Typeはfunctionのみのようです。DescriptionGet the current weather in a given locationTool Nameget_current_weatherParametersには以下のJSON Schemaを記述します。
{
  "type": "object",
  "properties": {
    "location": {
      "type": "string",
      "description": "The city name, e.g. Tokyo"
    }
  },
  "required": [
    "location"
  ]
}
作成をクリックします。


PL/SQLファンクションget_current_weatherが、呼び出し可能なツールとして登録されました。


実際に呼び出されるPL/SQLファンクションget_current_weatherを作成します。

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

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

また、weathercodeについては「気象関連コード表」に記載されているWMO 4501のHTML表を表WMO4501、WMO 4677のHTML表を表WMO4677として、データをロードします。双方の表は列CODE(NUMBER)、列DESCRIPTION(VARCHAR2(4000))の列を持ちます。データ・ワークショップを使ってHTML表をコピペし、それぞれの表にロードします。

ファンクションget_current_weatherの作成は以上です。

ページ・デザイナホーム・ページを開きます。

リージョンSystem Promptに、チャット呼び出しの際に含めるTool Setを選択するページ・アイテムを作成します。

識別名前P1_TOOL_SETタイプ選択リストラベルTool Setとします。

LOVタイプSQL問合せを選択し、SQL問合せとして以下を記述します。
select tool_set d, tool_set r from openai_tools group by tool_set
追加値の表示オフNULL値の表示オンとし、NULL表示値-- Select Tool Set --とします。


左ペインでプロセス・ビューを開き、プロセスSend Messageのパラメータp_tool_setを選択します。

タイプアイテムに変更し、アイテムとしてP1_TOOL_SETを指定します。


以上でツール呼び出しの組み込みは完了です。

アプリケーションを実行すると、記事の先頭にあるGIF動画のように動作します。

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

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

OpenAIのChat Completions APIを呼び出すAPEXアプリを作成する

以前にOpenAIChat Completions APIが使えるようになった頃に、同じテーマで記事を書いています。それから時間も経ち、OllamaLlama.cppといったOpenAIのChat Completions API互換のAPIをサポートするソフトウェアも利用できるようになりました。Chat Completions API自体には互換性があるため、これらを同様に扱えるひとつのAPEXアプリを作ってみることにしました。

とにかくChat Completions APIを呼び出せれば良いので、ユーザー・インターフェースの作り込みは最小限にします。APIの呼び出しはPL/SQLのパッケージUTL_OPENAI_CHAT_APIにまとめて実装しています。パッケージのコードは記事の末尾に添付します。パッケージUTL_OPENAI_CHAT_APIはツール呼び出しに関する実装も含んでいます。

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

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

今回はChat Completions APIを呼び出す対象をOpenAI、モデルは廉価なgpt-3.5-turboを使います。そのため、OpenAIのAPIキーをAPEXのWeb資格証明として作成しておきます。

ワークスペース・ユーティリティWeb資格証明を作成します。


Web資格証明名前OpenAI API Keyとしています。使用するWeb資格証明の指定には静的識別子を使用するため、この名前はなんでもかまいません。

静的識別子としてOPENAI_API_KEY認証タイプHTTPヘッダー資格証明名Authorizationとします。資格証明シークレットしてBearerで始めて空白で区切った後、OpenAIのAPIキーを続けた文字列を設定します。


アプリケーション・ビルダーからアプリケーションの作成を呼び出し、空のアプリケーションを作成します。

名前Chat with Generative AIとします。

アプリケーションの作成をクリックします。


空のアプリケーションが作成されます。Chat Completions APIを呼び出す機能はすべてホーム・ページに実装します。

最初に置換文字列に各種パラメータを設定します。アプリケーション定義の編集を開きます。


アプリケーション定義置換のセクションを開きます。

置換文字列として呼び出すサービスとモデルの指定に必要な情報を設定します。

呼び出す対象はOpenAIなので、G_API_ENDPOINTとしてhttps://api.openai.com/v1/chat/completionsを設定します。G_CHAT_HISTORYにはチャット履歴を保存するAPEXコレクションの名前としてCHAT_HISTORYを設定します。これはアプリケーション内で同じ名前のコレクションを参照させるためのもので、どのような名前でも構いませんが、あえて変更する必要もないでしょう。G_MODEL_NAMEgpt-3.5-turboを指定します。G_CREDENTIALとしてOpenAIのAPIキーを登録したWeb資格証明であるOPENAI_API_KEYを設定します。

以上を設定して、変更の適用をクリックします。


ページ・デザイナホーム・ページを開きます。

最初にチャット履歴を初期化するボタンを作成します。ボタンはブレッドクラムのリージョンChat with Generative AIに作成します。

識別ボタン名INITラベルStart New Conversationとします。レイアウト位置Nextを選択します。

動作アクションとしてこのアプリケーションのページにリダイレクトを選択します。


ターゲットをクリックしリンク・ビルダーを開きます。

ターゲットページ&APP_PAGE_ID.を記述します。これはこのボタンがあるページ番号、つまり自ページの呼び出しを指定しています。詳細リクエストINIT_CONVERSATIONを設定します。

OKをクリックし、リンク・ビルダーを閉じます。


このボタンを押した時に実行されるプロセスを作成します。

レンダリング前ヘッダーの前にプロセスを作成します。

識別名前Init Conversationとします。タイプコードの実行です。ソースPL/SQLコードとして以下を記述します。APEXコレクションが作成されていないか(サインイン直後)、またはREQUESTにINIT_CONVERSATIONが設定されている場合(ボタンStart New Conversationが押されたとき)に、APEXコレクションを初期化します。最初にロールがsystemの空のプロンプトを挿入します。このsystemプロンプトは変更できるように、画面にUIを実装します。



systemプロンプトを設定するUIを作成します。

新規にリージョンを作成します。

識別タイトルSystem Promptとします。タイプ静的コンテンツです。


作成したリージョンに、プロンプトを入力するページ・アイテムを作成します。

識別名前P1_PROMPTタイプテキスト領域とします。ラベルPromptです。セッション・ステートデータ型CLOBストレージセッションごと(永続)を選択します。


入力したsystemプロンプトをチャット履歴に設定するボタンを作成します。

識別ボタン名SET_PROMPTラベルSet Promptとします。動作アクションはデフォルトのページの送信です。


ボタンSET_PROMPTをクリックしたときに実行するプロセスを作成します。

左ペインでプロセス・ビューを開き、新規にプロセスを作成します。

識別名前Set Promptとします。タイプコードを実行です。ソースPL/SQLコードとして以下を記述します。すでに登録済みのsystemプロンプトを置き換えます。


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


ユーザーによるメッセージを送信するUIを作成します。

新規にリージョンを作成します。

識別タイトルUser Messageとします。タイプ静的コンテンツです。


作成したリージョンに、ユーザーによるメッセージを入力するページ・アイテムを作成します。

識別名前P1_MESSAGEタイプテキスト領域とします。ラベルMessageです。セッション・ステートデータ型CLOBストレージリクエストごと(メモリーのみ)を選択します。


入力したメッセージを含めて、Chat Completions APIを呼び出すボタンを作成します。

識別ボタン名SEND_MESSAGEラベルSend Messageとします。動作アクションはデフォルトのページの送信です。


Chat Completions APIのレスポンスを含めた、チャット履歴を表示するリージョンを作成します。

識別タイトルChat Historyタイプカードを選びます。ソースのタイプSQL問合せを選択し、SQL問合せとして以下を記述します。
select seq_id, c001, c002, clob001, n001, n002, n003
from apex_collections
where collection_name = :G_CHAT_HISTORY order by seq_id desc

カードに表示する内容を設定します。プロパティ・エディタ属性タブを開きます。

外観レイアウトとして水平(行)を選びます。カード主キー列1SEQ_IDです。

タイトルC001サブタイトルC002本体CLOB001を選択します。2次本体拡張フォーマットオンにし、HTML式として以下を記述します。
{if N001/}
prompt_tokens: &N001. completion_tokens: &N002. total_tokens: &N003.
{endif/}

デバッグ用にChat Completions APIに送信したリクエストと、受信したレスポンスを表示するページ・アイテムを作成します。

送信したリクエストを保持するページ・アイテムはP1_REQUESTとして作成します。ラベルRequest設定ページの送信時に送信オフにします。セッション・ステートデータ型CLOBストレージセッションごと(永続)を選択します。


同様にレスポンスを保持するページ・アイテムP1_RESPONSEを作成します。ラベルResponseとします。


左ペインでプロセス・ビューを開き、ボタンSEND_MESSAGEをクリックしたときに実行されるプロセスを作成します。

識別名前Send MessageタイプAPIの呼出しとします。設定パッケージUTL_OPENAI_CHAT_APIプロシージャまたはファンクションCHATを選択します。

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


パラメータp_contentタイプアイテムアイテムP1_MESSAGEとします。


パラメータp_collection_nameタイプ静的値静的値&G_CHAT_HISTORY.とします。


パラメータp_api_endpointタイプ静的値静的値&G_API_ENDPOINT.とします。


パラメータp_model_nameタイプ静的値静的値&G_MODEL_NAME.とします。


パラメータp_credential_static_idタイプ静的値静的値&G_CREDENTIAL.とします。


パラメータp_request_outの値のアイテムにP1_REQUESTを指定します。


パラメータp_response_outの値のアイテムにP1_RESPONSEを指定します。


その他のパラメータはAPIデフォルトのまま変更しません。

以上でOpenAIのChat Completions APIを呼び出すAPEXアプリは完成です。アプリケーションを実行すると、記事の先頭のGIF動画のように動作します。

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

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


コードにはツール呼び出しの対応が含まれていて、ツールに関する定義を保存する表OPENAI_TOOLSへの参照があります。そのため、以下のDDLを実行して表をあらかじめ作成しておく必要があります。