また、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を作成します。
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
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、静的IDはLOCAL_GRANITE_278M、ローカル埋込みのカスタム・ファンクション名にGENERATE_EMBEDDINGを設定します。
以上の設定で、ベクトル・プロバイダLocal Granite 278mを作成します。
続いてワークスペース・ユーティリティの生成AIを開き、生成AIサービスとして使用するChatモデルのGemma 3を構成します。
作成済みの生成AIサービスが一覧されます。作成をクリックします。
識別のAIプロバイダとしてOpen AIを選択します。名前はGemma-3-27b-it、静的IDはGEMMA3_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データ・ソース・タイプとして簡易HTTPを選択します。名前はGitHub APEXKBとします。PDF版の記事が置いてあるURLエンドポイントは以下です。
https://api.github.com/repos/ujnak/APEXKB/contents/
リージョンの表示と行選択を設定します。
次へ進みます。
リモート・サーバーがすでに登録されている場合は、そのまま使用します。そうでない場合は、リモート・サーバーは- 新規作成 -として、ベースURLはhttps://api.github.com/repos/、サービスURLパスはujnak/APEXKB/contents/とします。
次へ進みます。
ページ区切りタイプはページ区切りなしとします。
次へ進みます。
公開されているリポジトリなので認証は不要です。
検出をクリックします。
データのプレビューが表示されます。
RESTデータ・ソースの作成をクリックします。
以上でPDF版の記事を一覧するRESTデータ・ソースが作成されました。
このRESTデータ・ソースから記事を選択しデータベースへロードするページを作成します。
ページの作成をクリックします。
Content Rowを選択します。Content RowのリージョンにPDFドキュメントを指すURLを一覧し、行選択の機能を使ってデータベースにロードするドキュメントを選択します。
ページ番号は2、名前は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>とします。
記事のエントリ自体を小さくするため、AppearanceのStyleにCompactを選択し、Remove Paddingをオンにします。
行選択のタイプに複数選択を選び、現在の選択のページ・アイテムにP2_SELECTED、すべてのページ・アイテムの選択にP2_SELECT_ALLを指定します。
ページ区切りのタイプをスクロールに変更し、総数の表示をオンにします。
外観の表示は複数(レポート)です。設定のTitleは&NAME.、Descriptionは<a href="&DOWNLOAD_URL.">&DOWNLOAD_URL.</a>とします。
記事のエントリ自体を小さくするため、AppearanceのStyleにCompactを選択し、Remove Paddingをオンにします。
行選択のタイプに複数選択を選び、現在の選択のページ・アイテムにP2_SELECTED、すべてのページ・アイテムの選択にP2_SELECT_ALLを指定します。
ページ区切りのタイプをスクロールに変更し、総数の表示をオンにします。
ドキュメントの検索機能を追加します。
ブレッドクラムを選択し、タイプをスマート・フィルタに変更します。
ソースのフィルタ済リージョンにDocumentsを指定します。デフォルトで行検索に使用するページ・アイテムP2_SEARCHが作成されます。
レポート上で選択したドキュメントを、データベースにロードするボタンを作成します。
識別のボタン名はLOAD、ラベルはLoadとします。外観のホットをオン、テンプレート・オプションのWidthにStretchを指定します。動作のアクションはデフォルトのページの送信です。
識別の名前はLoad、ソースのPL/SQLコードとして以下を記述します。選択された複数の記事のダウンロードURLが、ページ・アイテムP2_SELECTEDに:(コロン)区切りで渡されます。それぞれのダウンロードURLごとにPDFの取得、テキストの抽出およびチャンク分割を行います。分割されたチャンクに対してベクトル・プロバイダを呼び出してエンべディングを生成し、表EBAJ_DOCUMENT_CHUNKSにチャンクと共に保存しています。
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_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ソースとして使用するにあたり、ページを作成して事前に検索結果を確認します。
ページの作成を呼び出し、空白ページを作成します。
ページの作成をクリックします。
空白ページが作成されます。
検索対象となる文章を入力するページ・アイテムを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問合せ、静的IDはAPEX_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を作成します。動作のアクションとして動的アクションで定義を選択します。
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のアプリケーション作成の参考になれば幸いです。
完