RAGアプリをOracle APEXの新機能で改善してみた その１ 対話型ダイアログの活用

以前Qiitaに投稿した全3回の記事で、23ai＋APEXを使ったRAGアプリ実装をまとめました。
- [Oracle Database 23ai FreeとAPEXでRAGを使った生成AIアプリをローコード開発してみた (事前準備編)](https://qiita.com/ssfujita/items/34d9e3c39ca730d29351)
- [Oracle Database 23ai FreeとAPEXでRAGを使った生成AIアプリをローコード開発してみた (アプリ実装 前編)](https://qiita.com/ssfujita/items/c53579bbb3eed4145942)
- [Oracle Database 23ai FreeとAPEXでRAGを使った生成AIアプリをローコード開発してみた (アプリ実装 後編)](https://qiita.com/ssfujita/items/53df4f342e28c238a5a2)

どんなものを実装したかと言うと、PDFやOfficeファイルなど任意のファイルをアップロードしてRAGに使えるアプリを作りました。

上記を作成した2024年5月時点では、APEXに生成AIを意識した専用機能が備わっていなかったため、既存機能でどうにか実装しました。
一方2024年7月にリリースされた APEX 24.1 からは、以下のような生成AIに特化した新機能が複数追加されています。
- 自然言語を使用したアプリケーションの作成
- 自然言語を使用したSQLの記述
- 対話型のAIダイアログ (いわゆるChatボット用のUI)
- APEX_AI APIの追加 (文章生成、チャット履歴保持などの機能の提供)

詳細は下記をご覧ください。
https://apex.oracle.com/ja/platform/features/whats-new-241/

今回はせっかく APEX 24.1 がリリースされたので、5月に作成したAPEXのRAGアプリを新機能で改善してみました。
また既存機能で改善した箇所もありますので、そちらも合わせてご紹介します。
以下は改善したアプリのイメージで、新機能の対話型ダイアログを使っています。
![app_img.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/a415215d-0e96-1013-8d47-88d5c909dda0.png)

作成したAPEXアプリのexportファイルは以下にありますので、必要に応じてご参照ください。
改善パターンとして2通り試しましたので、exportファイルは2つあります。
https://github.com/mago1chi/apex/blob/main/apex_2413_rag_chatbot.sql (対話型ダイアログを使った方法)
https://github.com/mago1chi/apex/blob/main/apex_2413_rag_chain.sql (APEX_AIパッケージを使った方法)

なお本記事では前者 (対話型ダイアログを使った方法) を扱います。
後者 (APEX_AIパッケージを使った方法) は次回まとめる予定です。

## 構成情報

環境は APEX 24.1 向けに新しく作ります。
APEX 24.1 に以前の記事で作成したアプリを export/import して作業を進めるイメージです。

今回は以下の構成で実装しました。
- VM: OCI上の VM.Standard.E4.Flex (1 OCPU、メモリ 16GB)
- OS: Oracle Linux 8.9
- DB: Oracle Database 23ai Free
- AP: Oracle APEX 24.1.3
- LLM: OCI Generative AI Service (Cohere Command R+)
- Embedding: OCI Generative AI Service (Cohere embed-multilingual-v3.0)

## APEX 24.1 の作成

まずは以下リンク先にある通常の手順で Oracle Database 23ai Free と APEX 24.1.1 をインストールします。
- [Oracle Database 23ai Freeインストール](https://qiita.com/ssfujita/items/34d9e3c39ca730d29351#oracle-database-23ai-free%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)
- [APEXインストール](https://qiita.com/ssfujita/items/34d9e3c39ca730d29351#apex%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)
 
APEXは 24.1 をインストールした初期バージョン 24.1.1 でもOKですが、今回はパッチを適用して 24.1.3 で実装します。
というのも、24.1.1 では OpenAI や本家の Cohere のモデルなどには対応していますが、私が使いたい OCI Generative AI Service を使った実装に未対応だからです。。
対応したのは 24.1.2 からで、本記事の執筆時点の最新が 24.1.3 のため、24.1.3 を利用します。

:::note info
上記からLLMに OCI Generative AI Service を使わない場合は 24.1.3 パッチ適用をスキップできます。
:::

手始めに APEX 24.1.3 のパッチを下記リンクより入手します。
https://support.oracle.com/epmos/faces/PatchDetail?patchId=36695709

:::note warn
アクセスにはMy Oracle Supportのアカウントが必要です
:::

入手したパッチをサーバにアップロードし、Oracleユーザで解凍します。

```bash
$ unzip p36695709_2410_Generic.zip
```

解凍して出来たフォルダへ移動します。

```bash
$ cd 36695709/
```

APEXをインストールしたPDBにログインします。

```bash
$ sqlplus / as sysdba
SQL> alter session set container=freedpdb1;
```

パッチを適用します。

```sql
SQL> @catpatch.sql
```

正常に適用できたか確認します。

```sql
SQL> select patch_version, installed_on
     from apex_patches
     where patch_number = 36695709;

PATCH_VERSION		       INSTALLED_ON
------------------------------ -------------------
3			       2024-09-08 08:32:09
```

## その他の構築作業

以前の記事を参考にORDSをセットアップします。
[ORDSインストール](https://qiita.com/ssfujita/items/34d9e3c39ca730d29351#ords%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)

RAGアプリで使うスキーマを作成します。
[アプリに使うスキーマの作成](https://qiita.com/ssfujita/items/34d9e3c39ca730d29351#%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AB%E4%BD%BF%E3%81%86%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E3%81%AE%E4%BD%9C%E6%88%90)

Embeddingに OCI Generative AI Service の Cohere Command R+ を使うため、資格証明をDBオブジェクトとして作成します。
APIキーの作成方法は下記をご参照ください。
[API署名キーの生成方法](https://docs.oracle.com/ja-jp/iaas/Content/API/Concepts/apisigningkey.htm#two)

```sql
DECLARE
  jo json_object_t;
BEGIN
  jo := json_object_t();
  jo.put('user_ocid','<ご自分のOCIアカウントのOCIDを入力>');
  jo.put('tenancy_ocid','<ご自分のOCIテナンシOCIDを入力>');
  jo.put('compartment_ocid','<ご自分のOCIコンパートメントOCIDを入力>');
  jo.put('private_key','<ご自分のOCIアカウントで作成したAPIキーを入力>');
  jo.put('fingerprint','<ご自分のOCIアカウントで作成したAPIキーに対応するフィンガープリントを入力>');
  dbms_output.put_line(jo.to_string);
  dbms_vector.create_credential(
    CREDENTIAL_NAME   => 'OCI_GENAI',
    PARAMS        => json(jo.to_string));
end;
/
```

APEXの管理画面にブラウザでアクセスし、ワークスペースを作成します。
[ワークスペースの作成](https://qiita.com/ssfujita/items/c53579bbb3eed4145942#%E3%83%AF%E3%83%BC%E3%82%AF%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9%E3%81%AE%E4%BD%9C%E6%88%90)

あとは以前作成したAPEXアプリのexportファイルを「アプリケーション・ビルダー」からimportします。
![import.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/877ccad9-3343-e741-307a-7014d6e1772a.png)
作成済みのexportファイルがない場合は下記から入手可能です。
https://github.com/mago1chi/apex/blob/main/ragapp_cohere.sql

(オプション) 応答速度が気になる場合は、ベクトルデータ用の索引を作っておきます。
今回はIVFを作成します。
RAGアプリ用のDBユーザでAPEXをインストールしたPDBにログインし、以下DDLを実行します。

```sql
SQL> CREATE VECTOR INDEX ivf_idx ON embed (embedding) ORGANIZATION NEIGHBOR PARTITIONS
     DISTANCE COSINE
     WITH TARGET ACCURACY 95;
```

## UIの編集

生成AIからの回答はモーダルダイアログ上に表示するため、回答表示用のアイテム「PX_ANSWER」は削除します。
※ページ番号は環境によって異なる可能性があるため、アイテム名は「PX_...」と表記しています。
![WS000000.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/b3696e8e-842d-c22a-b2d0-2e3adf6bc0db.jpeg)

対話型ダイアログを起動するためのボタンを追加します。
ボタン名は「AI_Assistant」とし、見た目は「検索」ボタンと同じようにします。
配置は好みに合わせて調整してください。
![WS000001.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/6c975a7a-2ba2-581e-c073-28c200394ca6.jpeg)

生成AIに連携するためのベクトル検索結果を格納するアイテム「PX_SEARCH_RES」を追加します。
値を格納する目的のみで使うため、タイプは「非表示」にします。
![WS000002.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/109cd873-6b76-a7e4-ee0c-747a1699f729.jpeg)

ベクトル検索結果を分かりやすく表示するためのアイテム「PX_CITATION」を削除します。
![WS000003.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/9ae864ab-947e-22eb-98b6-e0303ce1fb6c.jpeg)

代わりに「クラシック・レポート」のアイテム「ベクトル検索結果」を追加します。
![WS000004.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/dffd9a0b-9bb6-07f1-157e-c6d0047ea4dc.jpeg)
前回は「リッチ・テキスト」のアイテムに直接HTMLタグで表現したテーブルを作成し、ベクトル検索結果を表示していました。
ですがAPEXの既存機能であるAPEXコレクションを使った方が、よりシンプルに実装できるため今回変更しました。

なおアイテム「ベクトル検索結果」の「SQL問合せ」に入力するSQLは以下になります。

```sql
SELECT
       C001,
       C002
  FROM APEX_COLLECTIONS
WHERE collection_name = 'CITATION_RAG'
```

## ベクトル検索処理の改修

｢プロセス」タブに移り、「ベクトル検索」プロセスを改修します。
![WS000005.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/8838ea01-7397-94ca-fc88-95b8863bab7c.jpeg)

今回使用するコードは以下になります。

```sql
DECLARE
    l_context CLOB;
    l_prompt CLOB;
    cohere_params clob;
BEGIN
    -- OCI GenAI のEmbeddingを利用
    cohere_params := '
    { 
      "provider": "ocigenai",
      "credential_name": "OCI_GENAI", 
      "url": "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/embedText",
      "model": "cohere.embed-multilingual-v3.0"
    }';

    -- ベクトル検索結果を一時格納するAPEXコレクションを定義
    APEX_COLLECTION.CREATE_OR_TRUNCATE_COLLECTION(
        p_collection_name => 'CITATION_RAG'
    );
    
    -- ベクトル検索を実行し、RAGに使用するコンテキストを取得
    IF :P9_CATEGORY IS NULL THEN
        IF :P9_DOCNUM IS NULL THEN
            -- カテゴリ、保証ドキュメント数の指定がない場合のクエリ
            FOR rec IN (SELECT doc.doc_filename doc_filename, em.embed_data embed_data
                FROM documents doc, embed em 
                WHERE doc.id = em.doc_id AND TRUNC(doc.doc_lastupd) BETWEEN :P9_BEGIN_DATE AND :P9_END_DATE
                ORDER BY VECTOR_DISTANCE(em.embedding, TO_VECTOR(DBMS_VECTOR.UTL_TO_EMBEDDING(:P9_QUESTION, JSON(cohere_params))), COSINE)
                FETCH APPROXIMATE FIRST :P9_NUM ROWS ONLY)
            LOOP
                -- プロンプトに埋め込むコンテキストを追記
                l_context := l_context || rec.embed_data;
                -- 引用元情報をAPEXコレクションに格納
                apex_collection.add_member(
                      p_collection_name => 'CITATION_RAG',
                      p_c001 => rec.doc_filename,
                      p_c002 => rec.embed_data
                );
            END LOOP;
        ELSE
            -- カテゴリの指定がなく、保証ドキュメント数の指定がある場合のクエリ
            FOR rec IN (SELECT doc.doc_filename doc_filename, em.embed_data embed_data
                FROM documents doc, embed em 
                WHERE doc.id = em.doc_id AND TRUNC(doc.doc_lastupd) BETWEEN :P9_BEGIN_DATE AND :P9_END_DATE
                ORDER BY VECTOR_DISTANCE(em.embedding, TO_VECTOR(DBMS_VECTOR.UTL_TO_EMBEDDING(:P9_QUESTION, JSON(cohere_params))), COSINE)
                FETCH APPROXIMATE FIRST :P9_DOCNUM PARTITIONS BY doc.id, :P9_NUM ROWS ONLY)
            LOOP
                -- プロンプトに埋め込むコンテキストを追記
                l_context := l_context || rec.embed_data;
                -- 引用元情報をAPEXコレクションに格納
                apex_collection.add_member(
                      p_collection_name => 'CITATION_RAG',
                      p_c001 => rec.doc_filename,
                      p_c002 => rec.embed_data
                );
            END LOOP;
        END IF;
    ELSE
        IF :P9_DOCNUM IS NULL THEN
            -- カテゴリの指定があり、保証ドキュメント数の指定がない場合のクエリ
            FOR rec IN (SELECT doc.doc_filename doc_filename, em.embed_data embed_data
                FROM documents doc, embed em 
                WHERE doc.id = em.doc_id AND 
                doc.category IN (SELECT column_value FROM APEX_STRING.SPLIT(:P9_CATEGORY,':')) AND 
                TRUNC(doc.doc_lastupd) BETWEEN :P9_BEGIN_DATE AND :P9_END_DATE
                ORDER BY VECTOR_DISTANCE(em.embedding, TO_VECTOR(DBMS_VECTOR.UTL_TO_EMBEDDING(:P9_QUESTION, JSON(cohere_params))), COSINE)
                FETCH APPROXIMATE FIRST :P9_NUM ROWS ONLY)
            LOOP
                -- プロンプトに埋め込むコンテキストを追記
                l_context := l_context || rec.embed_data;
                -- 引用元情報をAPEXコレクションに格納
                apex_collection.add_member(
                      p_collection_name => 'CITATION_RAG',
                      p_c001 => rec.doc_filename,
                      p_c002 => rec.embed_data
                );
            END LOOP;
        ELSE
            -- カテゴリ、保証ドキュメント数の指定がある場合のクエリ
            FOR rec IN (SELECT doc.doc_filename doc_filename, em.embed_data embed_data
                FROM documents doc, embed em 
                WHERE doc.id = em.doc_id AND 
                doc.category IN (SELECT column_value FROM APEX_STRING.SPLIT(:P9_CATEGORY,':')) AND 
                TRUNC(doc.doc_lastupd) BETWEEN :P9_BEGIN_DATE AND :P9_END_DATE
                ORDER BY VECTOR_DISTANCE(em.embedding, TO_VECTOR(DBMS_VECTOR.UTL_TO_EMBEDDING(:P9_QUESTION, JSON(cohere_params))), COSINE)
                FETCH APPROXIMATE FIRST :P9_DOCNUM PARTITIONS BY doc.id, :P9_NUM ROWS ONLY)
            LOOP
                -- プロンプトに埋め込むコンテキストを追記
                l_context := l_context || rec.embed_data;
                -- 引用元情報をAPEXコレクションに格納
                apex_collection.add_member(
                      p_collection_name => 'CITATION_RAG',
                      p_c001 => rec.doc_filename,
                      p_c002 => rec.embed_data
                );
            END LOOP;
        END IF;
    END IF;
    
    -- OCI GenAIに連携するベクトル検索結果を非表示アイテムに格納
    :P9_SEARCH_RES := l_context;
END;
```

主な変更点は次の通りです。

- 7～13行目: OCI GenAIのEmbeddingを使用するための定義をJSONで記載
- 15～18行目: ベクトル検索結果を表示するために結果を一時格納するAPEXコレクションを定義
- 33～37行目: ベクトル検索結果を表示するために一時格納用のAPEXコレクションへデータ追加する処理 (以降も各条件毎に処理を記載)
- 100行目: 生成AIへ連携する用のベクトル検索結果を非表示アイテムに格納

## 対話型ダイアログの設定

次に今回メインの対話型ダイアログを設定していきます。

### OCI用のWeb資格証明の作成

対話型ダイアログを使って生成AIとチャットする機能を実装しますが、生成AIと連携するためにWeb資格証明を作成します。
今回はOCI Generative AI Serviceのモデルを使うため、OCI用のWeb資格証明を作ります。
詳細手順は私が以前まとめた以下の記事をご参照ください。
[APEXでWeb資格証明を作成](https://qiita.com/ssfujita/items/1004a19d97220dbaf658#apex%E3%81%A7web%E8%B3%87%E6%A0%BC%E8%A8%BC%E6%98%8E%E3%82%92%E4%BD%9C%E6%88%90)

### 連携する生成AIの設定

APEX 24.1 がリリースされるまではプロセスを作成し、REST APIを叩くPL/SQLのコードを書いて生成AIと連携していました。
24.1 からは生成AIと連携するための専用機能がAPEXに追加されていますので、今回はそちらを活用します。
まずはメニューバーの「アプリケーション・ビルダー」から「すべてのワークスペース・ユーティリティ」を選択します。
![WS000006.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/5f714c13-cdcd-392f-0d31-a1cae7355a37.jpeg)

｢生成AI」を選択します。
![WS000007.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/2878832a-fcf3-6ecf-519a-306a4b4b83bc.jpeg)

｢作成」を選択します。
![WS000008.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/8ad239da-619a-2347-9171-8c6c2cf4b181.jpeg)

赤枠部分を入力して「作成」を選択します。
![WS000009.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/88621901-88e8-d486-b6a0-839806ef223c.jpeg)
生成AIとして「Cohere Command R+」を使うため「モデルID」には `cohere.command-r-plus` と入力します。
｢資格証明」では先ほど作成したOCI用のWeb資格証明を選択します。

### 対話型ダイアログの設定

これで下準備が整ったので、対話型ダイアログを設定していきます。
ページ・デザイナの画面に戻り、動的プロセスのタブへ切り替えます。
｢クリック」のところを右クリックして「動的アクションの作成」を選択します。
![WS000010.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/68e705fe-c22f-65b2-efdd-dd5996ed07de.jpeg)

赤枠の部分を入力します。
![WS000011.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/fbdeb1fc-55c6-a019-9e36-9ca39bd95353.jpeg)
｢タイミング」にはUI編集で追加した「AI_Assistant」ボタンが押されたら実行するよう入力します。

Trueの場合の処理を赤枠のように入力します。
![WS000012.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/487c32bf-a841-54b3-53a2-5058c4479cf8.jpeg)
｢アクション」は「Open AIアシスタント」と入力します。
｢生成AI」は先ほど作成した「oci_cohere」を入力します。
｢システム・プロンプト」には以下のように入力します。
※`PX_SEARCH_RES` の `PX` はご自分のページ番号で置き換えます。

```text
次の文脈を利用して、全ての質問に答えてください。

'''
&PX_SEARCH_RES.
'''

もし質問に回答できない場合は「分かりません。」と回答してください。
```

これによって非表示アイテム「PX_SEARCH_RES」に格納されたベクトル検索結果が生成AIに連携され、回答で使用されます。

また「クイック・アクション」には以下を入力します。

```text
&P9_QUESTION.
```

![WS000013.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/60d3f593-5ec1-10c9-c81d-a6f5b6582d49.jpeg)

これでベクトル検索に使用したクエリを、簡単に生成AIへの質問に流用できます。
以下のようなイメージで、対話型ダイアログを開くとクリックするだけで質問できるようになります。
![WS000014.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/ef84d985-1b48-20af-85af-0e5dd7fd836f.jpeg)

以上で実装完了です。

ちなみに本アプリでは使っていませんが、下記赤枠にある「レスポンスの使用」という機能を使うと、生成AIの回答文に含まれる一部の値を抽出し、フォームの入力値として扱えるようになります。
![WS000018.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/2f135704-429f-9e83-e797-7e1f20ce2db3.jpeg)
詳細はマニュアルをご参照ください。
[給与を提案するAIアシスタントの作成](https://docs.oracle.com/cd/G13834_01/htmdb/including-generative-ai-in-applications.html#GUID-BD590E3A-A0B9-4418-AEFA-1EA0350E303A)

## 動作確認

実際に使ってみます。
最初に好きな資料をアップロードし、ベクトル化して保存します。
今回は2024年秋アニメのタイトル、あらすじがまとめられたテキストファイルをアップロードし、ベクトル化しました。
あらすじの平均的な長さを考慮し、チャンクサイズは `400`、オーバーラップサイズは `80` としました。
![WS000015.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/871ac961-6471-deff-5578-d2c968ac22af.jpeg)

適当な質問でベクトル検索します。
例えば以下のような質問を投げてみました。

```
異世界で主人公がパーティーから追放される内容のアニメはありますか？
```

![WS000016.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/68ba52b6-d546-838c-fa2b-cc8622a85d1d.jpeg)

次に「AI Assistant」ボタンを選択して対話型ダイアログを起動します。
ベクトル検索したときの質問や、追加の質問を投げてみます。
![WS000017.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/dc54ec73-de66-82b3-2554-f567de5f11b6.jpeg)
ベクトル検索結果をもとにした回答、すなわちRAGが実装できているのが見て取れます。
加えて、このようにチャット履歴を考慮した回答も返してくれます。
生成AIにチャット履歴を考慮した回答をさせるには、履歴を一時的にどこかへ保存し、2回目以降の質問において履歴情報を質問に付け加える処理が必要となります。
自前で対応するには手間が掛かりますが、対話型ダイアログの活用によって簡単に実装できます。

以上、APEX 24.1 から追加された対話型ダイアログを使ったRAGアプリ実装でした。
次回は同じく APEX 24.1 から追加された APEX_AI パッケージを使った実装をまとめます。
こちらはアプリの見た目は変わりませんが、記述するPL/SQLのコードをシンプルに出来る新機能です。

## 参考資料

- [Build an Innovative Q&A Interface Powered by Generative AI with Oracle APEX](https://oracle-livelabs.github.io/apex/nyc-genai-lab/workshops/tenancy/index.html?customTrackingParam=:ow:lp:cpo::::RC_WWMK211125P00013:llid=3947&lab=0-intro)