2025年3月18日火曜日

Oracle APEX 24.2のAI構成とAIアシスタントを使ってみる

Oracle APEX 24.2では共有コンポーネントとして生成AI構成が追加されました。OpenAIやCohereといったAIサービスに、システム・プロンプトようこそメッセージ、さらにプロンプトに挿入するデータを定義するRAGソースをまとめて、ひとつの構成として定義します。

また、Oracle APEX 24.1でOpen AIアシスタント(英語ではOpen AI Assistant)という名称だった動的アクションは、AIアシスタントの表示(英語ではShow AI Assistant)に変更されました。

以前の記事「Oracle APEX 24.1のAIアシスタントを使ってみる」では、ベクトル検索の結果をプロンプトに含まれるために、かなりのコーディングを行なっています。Oracle APEX 24.2では、生成AI構成RAGソースが含まれたこと、および、ベクトル・プロバイダの追加により、面倒なコーディングの多くが不要になりました。

本記事ではOracle APEX 24.1で実装したAPEX ChatのアプリケーションをAPEX 24.2で実装し直します。Oracle APEX 24.2の生成AI構成RAGソースの使用にフォーカスし、APEX 24.1からあった最初のプロンプトレスポンスの使用およびクイック・アクションについては実装を省きます。これらの機能の本来の用途は、ユーザー・インターフェースの拡張です。APEX 24.2からは、プロンプトの調整はRAGソースで行うようにします。

以前の記事では生成AIサービスとしてCohereを呼び出していましたが、今回はローカルのLM Studioで実行するモデルtext-embedding-granite-embedding-278m-multilingual@q8_0をembeddingの生成に使用し、gemma-3-27b-itをチャットに使用します。データ・ソースには、以前と同様に本ブログのPDF記事を使います。

GitHubにパブリックなリポジトリを作成して、そこにPDFファイルを置くことにより、自前のデータ・ソースを使用できます。

以下より、APEXアプリケーションの作成手順を紹介します。データ・ソース関連の構成は以前の記事と同じですが、他の記事を参照せずに作業を進められるように、手順が同じでも説明を省かないようにします。

LM Studioのローカル・サーバーにembeddingモデルとChatモデルをロードしておきます。ローカル・サーバーが接続を待ち受けるポートは8080としています。今回の作業で使用するモデルはすでに紹介していますが、異なるモデルを選択することもできます。その場合はOracle APEX側で作成するベクトル・プロバイダ(embeddingモデル)や生成AIサービス(Chatモデル)に設定するモデルを、使用しているものに合わせます。


最初にベクトル・プロバイダを作成します。

ワークスペース・ユーティリティベクトル・プロバイダを開きます。


作成済みのベクトル・プロバイダが一覧されます。作成をクリックします。


現時点ではベクトル・プロバイダプロバイダ・タイプ生成AIサービスを選択しAIプロバイダとしてOpen AIを選択すると、ベースURLに設定できるのはhttps://api.openai.com/v1のみで、LM StudioやOllamaなどのOpenAI互換APIのエンドポイントは設定できません。そのため、ワークアラウンドとして以下のPL/SQLファンクションGENERATE_EMBEDDINGを作成します。

create or replace function generate_embedding(
p_content in varchar2
)
return vector
as
C_ENDPOINT constant varchar2(80) := 'http://host.containers.internal:8080/v1/embeddings';
C_MODEL constant varchar2(80) := 'text-embedding-granite-embedding-278m-multilingual@q8_0';
l_request clob;
l_request_json json_object_t;
l_response clob;
l_response_json json_object_t;
l_data json_array_t;
l_embedding json_object_t;
l_array json_array_t;
l_vector clob;
v vector;
e_call_embeddings_failed exception;
begin
l_request_json := json_object_t();
l_request_json.put('input', p_content);
l_request_json.put('model', C_MODEL);
l_request := l_request_json.to_clob();
apex_web_service.set_request_headers('Content-Type', 'application/json');
l_response := apex_web_service.make_rest_request(
p_url => C_ENDPOINT
,p_http_method => 'POST'
,p_body => l_request
);
-- dbms_output.put_line(l_response);
if apex_web_service.g_status_code <> 200 then
raise e_call_embeddings_failed;
end if;
l_response_json := json_object_t(l_response);
l_data := l_response_json.get_array('data');
l_embedding := treat(l_data.get(0) as json_object_t);
l_array := l_embedding.get_array('embedding');
l_vector := l_array.to_clob();
-- dbms_output.put_line(l_vector);
v := to_vector(l_vector);
return v;
end;
/
プロバイダ・タイプカスタムPL/SQLを選択し、名前Local Granite 278m静的IDLOCAL_GRANITE_278Mローカル埋込みカスタム・ファンクション名GENERATE_EMBEDDINGを設定します。

以上の設定で、ベクトル・プロバイダLocal Granite 278m作成します。


続いてワークスペース・ユーティリティ生成AIを開き、生成AIサービスとして使用するChatモデルのGemma 3を構成します。


作成済みの生成AIサービスが一覧されます。作成をクリックします。


識別AIプロバイダとしてOpen AIを選択します。名前Gemma-3-27b-it静的IDGEMMA3_27B_ITとします。

Oracle APEXはローカルのpodmanでコンテナとして動作しているので、コンテナからホストで動作しているLM Studioに接続することになります。そのため、設定URLに以下を設定します。

http://host.containers.internal:8080/v1

LM Studioのローカル・サーバーを呼び出す際に資格証明は不要ですが、生成AIサービスの必須設定であるため、APIキー適当な文字列を入力し新規作成します。

詳細AIモデルとしてgemma-3-27b-itを設定します。

以上を設定し、接続のテストを行ったのち作成します。


以上で、ベクトル・プロバイダ生成AIサービスの準備ができました。

検索対象となるドキュメントのチャンクとそのエンべディングを保存する表EBAJ_DOCUMENT_CHUNKSを作成します。

以下のDDLを実行します。
create table ebaj_document_chunks (
    id                     number generated by default on null as identity
                           constraint ebaj_document_chunks_id_pk primary key,
    url                    varchar2(400 char),
    chunk_id               number,
    chunk                  varchar2(4000 char),
    chunk_vector           vector
);

空のAPEXアプリケーションを作成します。名前APEX Chatとします。


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


GitHubに、このブログ日日是Oracle APEXの記事のPDF版が置いてあります。今回は、このPDFをデータベースに知識として取り込みます。

GitHubのリポジトリを参照するRESTデータ・ソースを作成します。

共有コンポーネントREST データ・ソースを開きます。


作成をクリックします。


RESTデータ・ソースの作成最初からとします。

へ進みます。


RESTデータ・ソース・タイプとして簡易HTTPを選択します。名前GitHub APEXKBとします。PDF版の記事が置いてあるURLエンドポイントは以下です。

https://api.github.com/repos/ujnak/APEXKB/contents/

へ進みます。


リモート・サーバーがすでに登録されている場合は、そのまま使用します。そうでない場合は、リモート・サーバー- 新規作成 -として、ベースURLhttps://api.github.com/repos/サービスURLパスujnak/APEXKB/contents/とします。

へ進みます。


ページ区切りタイプページ区切りなしとします。

へ進みます。


公開されているリポジトリなので認証は不要です。

検出をクリックします。


データのプレビューが表示されます。

RESTデータ・ソースの作成をクリックします。


以上でPDF版の記事を一覧するRESTデータ・ソースが作成されました。


このRESTデータ・ソースから記事を選択しデータベースへロードするページを作成します。

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


Content Rowを選択します。Content RowのリージョンにPDFドキュメントを指すURLを一覧し、行選択の機能を使ってデータベースにロードするドキュメントを選択します。


ページ番号名前Documentsとします。

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


ページが作成されます。

タイプContent RowのリージョンDocumentsを選択します。ソース位置RESTソースを選択し、RESTソースに先ほど作成したGitHub APEXKBを設定します。


DOWNLOAD_URLを選択し、ソース主キーをオンにします。以上でContent Rowの基本的な設定は完了です。


Content Rowの行選択の機能を構成します。

Content Rowのレポートに一覧されている記事を、すべて選択するチェックボックスとなるページ・アイテムを作成します。

識別名前P2_SELECT_ALLタイプチェックボックスです。ラベルすべて選択とします。セッション・ステートストレージとしてリクエストごと(メモリーのみ)を選択します。


レポート上で選択された記事を保存するページ・アイテムを作成します。

識別名前P2_SELECTEDタイプ非表示とします。ブラウザ上の処理で値が変更されるため、設定保護された値オフにします。セッション・ステートデータ型CLOBストレージリクエストごと(メモリーのみ)を指定します。


リージョンの表示と行選択を設定します。

リージョンDocuments属性タブを開きます。

外観表示複数(レポート)です。設定Title&NAME.Description<a href="&DOWNLOAD_URL.">&DOWNLOAD_URL.</a>とします。

記事のエントリ自体を小さくするため、AppearanceStyleCompactを選択し、Remove Paddingオンにします。

行選択タイプ複数選択を選び、現在の選択のページ・アイテムP2_SELECTEDすべてのページ・アイテムの選択P2_SELECT_ALLを指定します。

ページ区切りタイプスクロールに変更し、総数の表示オンにします。


ドキュメントの検索機能を追加します。

ブレッドクラムを選択し、タイプスマート・フィルタに変更します。

ソースフィルタ済リージョンDocumentsを指定します。デフォルトで行検索に使用するページ・アイテムP2_SEARCHが作成されます。


ここまでで、タイプがContent RowのリージョンへのRESTデータ・ソースの一覧と検索、および行選択ができるようになっています。


レポート上で選択したドキュメントを、データベースにロードするボタンを作成します。

識別ボタン名LOADラベルLoadとします。外観ホットオンテンプレート・オプションWidthStretchを指定します。動作アクションはデフォルトのページの送信です。


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

識別名前LoadソースPL/SQLコードとして以下を記述します。選択された複数の記事のダウンロードURLが、ページ・アイテムP2_SELECTEDに:(コロン)区切りで渡されます。それぞれのダウンロードURLごとにPDFの取得、テキストの抽出およびチャンク分割を行います。分割されたチャンクに対してベクトル・プロバイダを呼び出してエンべディングを生成し、表EBAJ_DOCUMENT_CHUNKSにチャンクと共に保存しています。

declare
l_docs apex_t_varchar2;
l_content blob;
l_text clob;
l_url varchar2(4000);
l_vector vector;
e_get_document_failed exception;
begin
/*
* ドキュメントが何も選択されていなければ処理をスキップ。
*/
if :P2_SELECTED is null then
apex_debug.info('Do nothing!');
return;
end if;
/*
* それぞれのダウンロードURLをhttps:のコロンで区切られないように .pdf: で区切る。
* 一番最後のファイルは.pdfで : がないため、拡張子は削除されない。
*/
l_docs := apex_string.split(:P2_SELECTED, '\.pdf\:');
for i in l_docs.first .. l_docs.last
loop
/*
* ダウンロードするPDFファイルごとの処理
*/
l_url := l_docs(i);
if substr(l_url, -4) <> '.pdf' then
/* 削除された拡張子.pdfを戻す */
l_url := l_url || '.pdf';
end if;
/*
* GitHubからPDFファイルをダウンロードする。
* ダウンロードしたファイルはl_contentにBLOBとして保存される。
*/
l_url := utl_url.escape(l_url, false, 'AL32UTF8');
apex_debug.info('Download PDF file from GitHub, %s', l_url);
apex_web_service.clear_request_headers();
l_content := apex_web_service.make_rest_request_b(
p_url => l_url
,p_http_method => 'GET'
);
if apex_web_service.g_status_code <> 200 then
raise e_get_document_failed;
end if;
/*
* PDFファイルからテキストを取り出す。
*/
l_text := dbms_vector_chain.utl_to_text(
data => l_content
,params => JSON(
json_object(
key 'plaintext' value true
)
)
);
apex_debug.info('Text retrieved length = %s', dbms_lob.getlength(l_text));
for r in (
select rownum, t.chunk_offset, t.chunk_length, t.chunk_text
from vector_chunks(
l_text
by words
max 500
overlap 0
split by recursively
language japanese
normalize all
) t
)
loop
apex_debug.info('ID %s, Chunk Offset %s, Length %s', r.rownum, r.chunk_offset, r.chunk_length);
l_vector := apex_ai.get_vector_embeddings(
p_value => r.chunk_text
,p_service_static_id => 'LOCAL_GRANITE_278M'
);
insert into ebaj_document_chunks(url, chunk, chunk_vector, chunk_id) values(l_url, r.chunk_text, l_vector, r.rownum);
end loop;
end loop;
end;
サーバー側の条件ボタン押下時LOADを指定します。


作成したページを実行し、これから実装するベクトル検索で使用するデータを準備します。

タイトルにGoogleを含む記事を検索し、すべて選択します。その後、ボタンLoadをクリックして、データベースに選択したドキュメントのチャンクとエンべディングをロードします。


表EBAJ_DOCUMENT_CHUNKSの内容を確認します。

select count(*) from ebaj_document_chunks where chunk_vector is not null;

列CHUNK_VECTORに値が設定されていることが確認できます。


この列CHUNK_VECTORを対象とした検索構成を作成します。

共有コンポーネント構成の検索を開きます。


作成をクリックします。


作成する検索構成名前記事検索 - ベクトルとします。検索タイプOracleベクトル検索です。

へ進みます。


ベクトル・プロバイダLocal Granite 278mを選択します。これは、ドキュメントをチャンク分割した後にエンべディングを生成するために呼び出したベクトル・プロバイダと同じものを指定します。

表/ビューの名前EBAJ_DOCUMENT_CHUNKSを指定します。

へ進みます。


主キー列ID(Number)ベクトル列CHUNK_VECTOR(Vector)タイトル列URL(Varchar2)説明列CHUNK(Varchar2)を指定します。

以上で検索構成の作成をクリックします。


作成した検索構成は生成AI構成RAGソースに使用します。APEX_SEARCH.SEARCHから検索構成を呼び出す際に静的IDを指定するため、静的IDとしてCHUNK_VECTOR_SEARCHを設定します。RAGソースとして取り込むのは説明列(列DESCRIPTIONとして参照される)のみです。この元列は表EBAJ_DOCUMENT_CHUNKSの列CHUNKSです。


アイコンと表示のセクションにある最大ベクトル距離.3返される最大行数6を設定します。この数値を調整するとこで、RAGソースとしてプロンプトに含まれるチャンクの数が変わります。


検索構成RAGソースとして使用するにあたり、ページを作成して事前に検索結果を確認します。

ページの作成を呼び出し、空白ページを作成します。


ページ定義名前RAGソース確認とします。

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


空白ページが作成されます。

検索対象となる文章を入力するページ・アイテムをP3_SENTENCEとして作成します。タイプテキスト・フィールドラベルSentenceとします。

設定[Enter]を押すと送信オンにし、セッション・ステートストレージセッションごと(永続)を選択します。ページ・アイテムP3_SENTENCEに文章を入力しEnterを押すと、P3_SENTECEの文章がサーバーに送信され、セッション・ステートに保存されます。そのため、レポートのソースよりP3_SENTECEの値を参照できます。


RAGソースに指定するSQLをソースSQL問合せに設定した、クラシック・レポートのリージョンを作成します。識別名前Chunksとします。

ソースSQL問合せに以下を記述します。
select description from table ( apex_search.search(
    p_search_static_ids => apex_t_varchar2('CHUNK_VECTOR_SEARCH')
    ,p_search_expression => :P3_SENTENCE
))
外観テンプレートに修飾の少ないInteractive Reportを選択します。


列DESCRIPTIONがAPEX_SEARCH.SEARCHでの出力順で表示されるように、列DESCRIPTIONソートソート可能オフにします。


以上で、検索構成を使ったRAGソースの確認ページは完成です。

ページを実行し、文章を入力して検索結果を確認します。


AIアシスタントに設定する生成AI構成を作成します。

共有コンポーネントAI構成を開きます。


作成をクリックします。


識別名前APEX問合せ静的IDAPEX_QAとします。生成AIサービスとしてGemma-3-27b-itを選択します。システム・プロンプトは「あなたはOracle APEXの専門家です。」、ようこそメッセージは「Oracle APEXに関する質問を受け付けます。」とします。

以上で作成をクリックします。


生成AI構成が作成されると、構成にRAGソースのセクションが現れます。RAGソースの作成をクリックします。


RAGソース識別名前PDFドキュメントとします。説明には「Oracle APEXのアプリケーション作成手順を解説しているドキュメントです。」と記述します。

ソースタイプSQL問合せを選択し、SQL問合せとして先ほど確認したSELECT文を記述します。引数p_search_expressionにはAIアシスタントに入力されたプロンプト:APEX$AI_LAST_USER_PROMPTを指定します。
select description from table ( apex_search.search(
    p_search_static_ids => apex_t_varchar2('CHUNK_VECTOR_SEARCH')
    ,p_search_expression => :APEX$AI_LAST_USER_PROMPT
))
以上で作成をクリックします。


RAGソースタイプには、SQL問合せの他に、ファンクション本体静的を選ぶことができます。またAIアシスタントに入力したプロンプトは、直近のユーザー・プロンプトを参照するAPEX$AI_LAST_USER_PROMPTの他に、それまでに入力したユーザー・プロンプトを連結した文字列としてAPEX$AI_ALL_USER_PROMPTSを参照できます。

以上で生成AI構成RAGソースが追加されました。ひとつの生成AI構成に複数のRAGソースを作成できます。また、RAGソースにはサーバー側の条件を設定できるため、ユーザーが入力したプロンプトやその他の条件に従って、RAGソースをプロンプトに含めるかどうか変えることができます。

変更の適用をクリックします。


以上で生成AI構成が作成できました。

作成した生成AI構成を使って、AIアシスタントを実装します。

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

AIアシスタントとしてチャットを行なうリージョンを作成します。今回はAIアシスタントインラインで表示します。

タイプ静的コンテンツのリージョンを作成します。名前AIアシスタントとします。外観テンプレートに装飾の少ないBlank with Attributesを選択します。

AIアシスタントインラインで表示するため、リージョンの静的IDとしてchatを設定します。


AIアシスタントを開始するボタンSTART_CHATを作成します。動作アクションとして動的アクションで定義を選択します。


ボタンSTART_CHAT動的アクションを作成します。識別名前onClick Start Chatタイミングイベントはボタンのデフォルトであるクリックです。


TRUEアクションとしてAIアシスタントの表示を選択します。生成AI構成としてAPEX問合せを選択します。外観表示形式インラインを選択し、コンテナ・セレクタ#chatを指定します。


以上でアプリケーションは完成です。

AIアシスタントの画面は以下のように動作します。


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

RAGソースを含んだ生成AI構成で、実際にLLMへ送信されるメッセージを確認しました。

確認のためにRAGソースとしてテキスト1テキスト2テキスト3を作成しました。PDFドキュメントについては、サーバー側の条件なしを設定し、メッセージから除外しています。


それぞれのRAGソースは、ソースタイプ静的を選択し、名前と同じテキストを返すようにしています。テキスト3に限り、サーバー側の条件を設定し、最後のユーザー・プロンプトtext3が含まれるときに限り、RAGソースとして選択されるように条件を付けています。


ユーザーが最初のプロンプトとして「こんにちは」を送信したときに、LLMへは以下のメッセージが送信されていました。生成AI構成システム・プロンプトに続けて、RAGソースが同じくroleがsystemのプロンプトとしてメッセージに含まれています。RAGソース説明ソースがcontentに含まれます。
{
  "messages": [
    {
      "role": "system",
      "content": "あなたはOracle APEXの専門家です。"
    },
    {
      "role": "system",
      "content": "このテキストは無視してください。\n\"\"\"\nテキスト1\"\"\"\n"
    },
    {
      "role": "system",
      "content": "このテキストは無視してください。\n\"\"\"\nテキスト2\"\"\"\n"
    },
    {
      "role": "user",
      "content": "こんにちは"
    } 
  ],  
  "model": "gemma-3-27b-it"
}
継続したチャットでユーザーがプロンプトとして「セキュリティ対策について教えて。text3」と入力すると、LLMへは以下のメッセージが送信されていました。RAGソースはメッセージを送信するたびに評価され、システム・プロンプトとしてメッセージの先頭に含まれるようです。それに続けて、チャットの履歴が送信メッセージに含まれます。
{
  "messages": [
    { 
      "role": "system", 
      "content": "あなたはOracle APEXの専門家です。"
    },
    {
      "role": "system",
      "content": "このテキストは無視してください。\n\"\"\"\nテキスト1\"\"\"\n"
    },
    {   
      "role": "system",
      "content": "このテキストは無視してください。\n\"\"\"\nテキスト2\"\"\"\n"
    },  
    {
      "role": "system", 
      "content": "このテキストは無視してください。\n\"\"\"\nテキスト3\"\"\"\n"
    },
    {
      "role": "user",
      "content": "こんにちは"
    },
    {
      "role": "assistant",
      "content": "はい、こんにちは! Oracle APEX の専門家として、どのようなご質問でもお気軽にお尋ねくださ... 「長いのでログから省略」 ...があれば、遠慮なく質問してください。 具体的な状況を教えていただけると、より的確な回答が可能です。\n"
    },
    {
      "role": "user",
      "content": "セキュリティ対策について教えて。text3"
    }
  ],
  "model": "gemma-3-27b-it"
}

今回の記事は以上になります。

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