作成したAPEXアプリケーションは以下のように動きます。ローカルLLMへのプロンプトに含めた画像が正しく解釈されるか確認するためのアプリなので、凝ったことはしていません。
イメージを含めたプロンプトはLM StudioのUIからも送信できるのでAPEXでアプリケーションを作らなくてもいいのですが、PL/SQLコードのテストにはなります。
作成したAPEXアプリケーションのエクスポートを以下に置きました。実質的にホーム・ページだけのアプリケーションです。
https://github.com/ujnak/apexapps/blob/master/exports/local-lm-studio-multimodal-gemma3.zip
以下にアプリケーションについて簡単に説明します。
すべての機能はホーム・ページに実装しています。
写真をアップロードするページ・アイテムとしてP1_PHOTOを作成しています。タイプはイメージ・アップロードです。
ストレージのタイプに表APEX_APPLICATION_TEMP_FILES、ファイルをパージするタイミングとしてリクエストの終わりを選択しています。セッション・ステートのストレージはリクエストごと(メモリーのみ)です。
プロンプトを入力するページ・アイテムとしてP1_PROMPTを作成しています。タイプはテキスト領域です。
セッション・ステートのデータ型はVARCHAR2、ストレージはセッションごと(永続)として、一度入力したプロンプトをセッション・ステートに保存するようにしています。
ページ・アイテムP1_PHOTOに設定した写真とP1_PROMPTに記述したプロンプトをサーバーに送信するボタンとしてSUBMITを作成しています。動作のアクションはページの送信です。
LLMからの応答を表示するページ・アイテムとしてP1_RESPONSEを作成しています。タイプはリッチ・テキスト・エディタです。
設定の書式にマークダウンを選択しています。セッション・ステートのデータ型はCLOB、ストレージはセッションごと(永続)です。読取り専用に常時を設定しています。
タイプはコードを実行、ソースの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 | |
/* Local LM Studio */ | |
C_ENDPOINT constant varchar2(4000) := 'http://host.containers.internal:8080/v1/chat/completions'; | |
/* LLM model name */ | |
C_MODEL constant varchar2(200) := 'gemma-3-27b-it'; | |
/* 写真に関する変数 */ | |
l_image json_object_t; | |
l_image_url json_object_t; | |
l_mime_type apex_application_temp_files.mime_type%type; | |
l_blob_content apex_application_temp_files.blob_content%type; | |
/* 送信するメッセージ */ | |
l_prompt json_object_t; | |
l_content json_array_t; | |
l_message json_object_t; | |
l_messages json_array_t; | |
l_request json_object_t; | |
l_request_clob clob; | |
/* 受信するメッセージ */ | |
l_response clob; | |
l_response_json json_object_t; | |
e_call_api_failed exception; | |
l_choices json_array_t; | |
l_choice json_object_t; | |
l_response_message clob; | |
begin | |
/* 写真の取り出し */ | |
select blob_content, mime_type into l_blob_content, l_mime_type | |
from apex_application_temp_files where name = :P1_PHOTO; | |
apex_debug.info('image type = %s, length = %s', l_mime_type, dbms_lob.getlength(l_blob_content)); | |
/* イメージの準備 - base64で送信する */ | |
l_image_url := json_object_t(); | |
/* | |
* LM Studioではimage_urlオブジェクトに直接base64の文字列を入れるのは不可で、 | |
* urlオブジェクトにbase64の文字列を入れて、image_urlに与える必要がある。 | |
*/ | |
l_image_url.put('url', 'data:' || l_mime_type || ';base64,' | |
|| apex_web_service.blob2clobbase64(l_blob_content, 'N', 'N') | |
); | |
l_image := json_object_t(); | |
l_image.put('type', 'image_url'); | |
l_image.put('image_url', l_image_url); | |
/* プロンプトの準備 */ | |
l_prompt := json_object_t(); | |
l_prompt.put('type', 'text'); | |
l_prompt.put('text', :P1_PROMPT); | |
/* contentの作成 */ | |
l_content := json_array_t(); | |
l_content.append(l_prompt); | |
l_content.append(l_image); | |
/* messageの作成 - systemプロンプトは無し, userの単発のみ */ | |
l_message := json_object_t(); | |
l_message.put('role', 'user'); | |
l_message.put('content', l_content); | |
/* messagesの作成 */ | |
l_messages := json_array_t(); | |
l_messages.append(l_message); | |
/* requestの作成 */ | |
l_request := json_object_t(); | |
l_request.put('model', C_MODEL); /* 使用するモデルはGemma 3 */ | |
l_request.put('messages', l_messages); | |
-- l_request.put('max_tokens', p_max_tokens); | |
/* call OpenAI chat completions api */ | |
l_request_clob := l_request.to_clob(); | |
apex_web_service.clear_request_headers(); | |
apex_web_service.set_request_headers('Content-Type', 'application/json', p_reset => false); | |
l_response := apex_web_service.make_rest_request( | |
p_url => C_ENDPOINT | |
,p_http_method => 'POST' | |
,p_body => l_request_clob | |
); | |
/* process response */ | |
if apex_web_service.g_status_code <> 200 then | |
raise e_call_api_failed; | |
end if; | |
l_response_json := json_object_t(l_response); | |
l_choices := l_response_json.get_array('choices'); | |
l_response_message := ''; | |
for i in 1..l_choices.get_size() | |
loop | |
l_choice := treat(l_choices.get(i-1) as json_object_t); | |
l_message := l_choice.get_object('message'); | |
l_response_message := l_response_message || l_message.get_clob('content'); | |
end loop; | |
:P1_RESPONSE := l_response_message; | |
end; |