作成するアプリケーションは以下のように動作します。AIアシスタントには、色々とカスタマイズするためのオプションがあります。オプションの設定によって、チャットの使い勝手や動作を変えることができるため、この動画とは異なるユーザー・インターフェースにもできます。
以下の画面では質問文からエンべディングを生成し、データベースに保存した意味的に近いチャンクを取り出し、最初のプロンプトとしてLLMに送信するメッセージに含めています。ユーザーとのチャットによるやり取りは、その後のメッセージとして追加されます。
名前はCOHERE_API_KEY、静的IDもCOHERE_API_KEYとします。認証タイプはHTTPヘッダー、資格証明名(つまりHTTPヘッダー名)としてAuthorizationを指定します。
資格証明シークレットしてBearerで始めて空白で区切り、CohereのTrial Keyを続けた文字列を設定します。
URLに対して有効はhttps://api.cohere.aiとします。
ワークスペース・ユーティリティの生成AIとして、Cohereを作成します。
AIプロバイダはCohere、名前はCohere、静的IDはCOHERE_AIとします。
アプリケービョン・ビルダーで使用はオフ、資格証明は先ほど作成したCOHERE_API_KEYを選択します。AIモデルとしてCohereで最も性能のよいcommand-r-plusを指定します。
これからAPEXアプリケーションを作成します。
最初に、以下のDDLを実行して、チャンクとエンべディングを保存する表EBMJ_EMBEDDINGSを作成します。URLを保存する表とチャンクとエンべディングを保存する表で親子関係の表を作るべきですが、省略して1つの表に保存しています。
create table ebmj_embeddings (
id number generated by default on null as identity
constraint ebmj_embeddings_id_pk primary key,
url varchar2(400 char),
chunk_id number,
chunk varchar2(4000 char),
embedding vector
);
APEXアプリケーションを作成します。
名前はAPEX Chatとし、アプリケーション作成ウィザードを起動します。
ページ名はDocumentsとします。このページには、PDFを選択してデータベースにロードする機能を実装します。
ページの追加をクリックします。
対話グリッドを選択します。
ページの追加をクリックします。
以上のページ構成で、アプリケーションの作成を実行します。
APEXアプリケーションが作成されます。
GitHubに、このブログ日日是Oracle APEXの記事のPDF版が置いてあります。今回は、このPDFをデータベースに知識として取り込むことにします。
GitHubのリポジトリを参照するRESTデータ・ソースを作成します。
共有コンポーネントの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データ・ソースから記事を選択しデータベースへロードする機能を、ページ番号2のDocumentsのページに実装します。
記事の一覧にはタイプがContent Rowのリージョンを使います。Oracle APEX 24.1より、Content Rowで行選択ができる新機能が追加されました。その機能を使うため、選択された行を保存するページ・アイテムと、すべての行を選択するチェックボックスを作成します。
ページ・デザイナでページDocumentsを開きます。
ページ上で選択した記事を、データベースにロードするボタンを作成します。
識別のボタン名はLOAD、ラベルはLoadとします。外観のホットをオン、テンプレート・オプションではWidthにStretchを指定します。動作のアクションはデフォルトのページの送信です。
レポートに一覧されている記事を、すべて選択するチェックボックスとなるページ・アイテムを作成します。
識別の名前はP2_SELECT_ALL、タイプはチェックボックです。ラベルはすべて選択とします。セッション・ステートのストレージとしてリクエストごと(メモリーのみ)を選択します。
レポート上で選択された記事を保持するページ・アイテムを作成します。
識別の名前はP2_SELECTED、タイプは非表示とします。ブラウザ上の処理で値が変更されるため、設定の保護された値はオフにします。セッション・ステートのデータ型はCLOB、ストレージはリクエストごと(メモリーのみ)を指定します。
記事の一覧と選択を行うリージョンを作成します。
識別の名前はDocumentsとします。タイプにContent Rowを選択します。
ソースの位置はRESTソース、RESTソースとして先ほど作成したGitHub APEXKBを選択します。
外観の表示は複数(レポート)です。設定の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、ソースのPL/SQLコードとして以下を記述します。選択された複数の記事のダウンロードURLが、ページ・アイテムP2_SELECTEDに:(コロン)区切りで渡されます。それぞれのダウンロードURLごとにPDFの取得、テキストの抽出およびチャンク分割を行います。分割されたチャンクをまとめてCohereのEmbed APIを呼び出してエンべディングを生成し、表EBMJ_EMBEDDINGSに保存しています。
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_chunks vector_array_t; | |
l_url varchar2(4000); | |
e_get_document_failed exception; | |
l_chunk_count integer; | |
l_chunk_text apex_t_varchar2; | |
l_call_count integer; | |
/* | |
* CohereのEmbed APIを呼び出し、引数のチャンクからエンベディングを生成する。 | |
* 生成したエンベディングは表EBMJ_EMBEDDINGSへ保存する。 | |
*/ | |
procedure generate_embeddings( | |
p_chunk_count in integer | |
,p_chunk_text in apex_t_varchar2 | |
,p_call_count in integer default 0 | |
) | |
as | |
l_request_json json_object_t; | |
l_request clob; | |
l_texts json_array_t; | |
l_response clob; | |
l_response_json json_object_t; | |
l_embeddings json_array_t; | |
l_embedding json_array_t; | |
e_embed_failed exception; | |
l_chunk ebmj_embeddings.chunk%type; | |
l_embed ebmj_embeddings.embedding%type; | |
l_chunk_id ebmj_embeddings.chunk_id%type; | |
begin | |
/* | |
* Cohere Embed APIに送信するリクエストを生成する。 | |
*/ | |
l_request_json := json_object_t(); | |
l_request_json.put('model', 'embed-multilingual-light-v3.0'); | |
l_request_json.put('input_type', 'search_document'); | |
l_texts := json_array_t(); | |
for i in 1 .. p_chunk_count | |
loop | |
l_texts.append(p_chunk_text(i)); | |
end loop; | |
l_request_json.put('texts', l_texts); | |
l_request := l_request_json.to_clob(); | |
/* | |
* Cohere Embed APIを呼び出す。 | |
*/ | |
apex_web_service.clear_request_headers; | |
apex_web_service.set_request_headers('Accept','application/json', p_reset => false); | |
apex_web_service.set_request_headers('Content-Type','application/json', p_reset => false); | |
l_response := apex_web_service.make_rest_request( | |
p_url => 'https://api.cohere.ai/v1/embed' | |
,p_http_method => 'POST' | |
,p_body => l_request | |
,p_credential_static_id => 'COHERE_API_KEY' | |
); | |
if apex_web_service.g_status_code <> 200 then | |
apex_debug.info(l_response); | |
raise e_embed_failed; | |
end if; | |
/* | |
* レスポンスからエンベディングを取り出し、表EBMJ_EMBEDDINGSへ保存する。 | |
*/ | |
l_response_json := json_object_t.parse(l_response); | |
l_embeddings := l_response_json.get_array('embeddings'); | |
for i in 1 .. p_chunk_count | |
loop | |
l_chunk := l_texts.get(i-1).to_string(); | |
l_embed := to_vector(l_embeddings.get(i-1).to_string()); | |
l_chunk_id := (p_call_count * 96) + i; | |
insert into ebmj_embeddings(url, chunk, embedding, chunk_id) values(l_url, l_chunk, l_embed, l_chunk_id); | |
-- apex_debug.info('url %s, id %s, chunk %s, embed %s', l_url, l_chunk_id, l_chunk, from_vector(l_embed)); | |
end loop; | |
end generate_embeddings; | |
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)); | |
/* | |
* 取り出したテキストをチャンクに分割する。チャンクごとにエンベディングを生成するが、 | |
* CohereのAPI呼び出しの回数を減らすため、最大96のチャンクをまとめて、APIを呼び出す。 | |
*/ | |
l_chunk_count := 0; | |
l_chunk_text := apex_t_varchar2(); | |
l_call_count := 0; | |
for r in ( | |
/* | |
* チャンクは500ワードごとにしている。CohereのEmbed APIのパラメータ texts の | |
* 説明: | |
* An array of strings for the model to embed. Maximum number of texts per call is 96. | |
* We recommend reducing the length of each text to be under 512 tokens for optimal quality. | |
*/ | |
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_chunk_count := l_chunk_count + 1; | |
apex_string.push(l_chunk_text, r.chunk_text); | |
/* | |
* チャンクの個数が96でCohereのEmbed APIを呼び出す。 | |
*/ | |
if l_chunk_count = 96 then | |
generate_embeddings( | |
p_chunk_count => l_chunk_count | |
,p_chunk_text => l_chunk_text | |
,p_call_count => l_call_count | |
); | |
dbms_session.sleep(1); /* CohereのFree枠に収めるため、1秒待つ */ | |
l_chunk_count := 0; | |
l_chunk_text := apex_t_varchar2(); | |
l_call_count := l_call_count + 1; | |
end if; | |
end loop; | |
/* | |
* 残りのチャンクを対象に、CohereのEmbed APIを呼び出す。 | |
*/ | |
if l_chunk_count > 0 then | |
generate_embeddings( | |
p_chunk_count => l_chunk_count | |
,p_chunk_text => l_chunk_text | |
,p_call_count => l_call_count | |
); | |
dbms_session.sleep(1); | |
end if; | |
end loop; | |
end; |
サーバー側の条件のボタン押下時にLOADを指定します。
以上でPDF記事を選択して、データベースにロードする機能の実装は完了です。
タイトルにGoogleが含まれている記事を、データベースにロードしてみます。
これから、本記事の本題であるAIアシスタントを実装します。
ページ・デザイナでホーム・ページを開き、デフォルトで作成されているページ・ナビゲーションを削除します。
AIへの質問を入力するページ・アイテムを作成します。
識別の名前はP1_QUESTION、タイプはテキスト領域とします。ラベルは質問とします。
AIとのチャットを開始するボタンを作成します。
識別のボタン名はASK、ラベルは問い合わせるとします。動作のアクションは動的アクションで定義を選択します。ボタンをクリックしたときに、動的アクションとして実装されているOpen AIアシスタントを実行します。
AIアシスタントの表示形式として、ダイアログとインラインの2種類を選択できます。今回は、まず最初にインラインを実装します。そのために、AIアシスタントとの会話を扱うリージョンを作成します。
識別の名前はAIチャットとします。タイプは静的コンテンツです。リージョンとしての装飾は不要なので、外観のテンプレートにBlank with Attributesを選択します。
詳細の静的IDとしてchatを指定します。AIアシスタントには、このリージョンchatを会話に使うように指示します。
AIアシスタントとの会話の結果、採用した回答を保存するページ・アイテムを作成します。
識別の名前はP1_RESP、タイプはテキスト領域とします。ラベルは回答とします。
セッション・ステートのデータ型はVARCHAR2、ストレージはセッションごと(永続)を選択します。
データベースに保存されたチャンクより、質問の内容に近いものを検索した結果を保存するページ・アイテムを作成します。
識別の名前はP1_PROMPT、タイプはMarkdownエディタを選択します。ただし、これは検索結果を確認するために、画面に表示されるページ・アイテムを選んでいます。通常は非表示にするのが適切でしょう。
ラベルは知識とし、セッション・ステートのデータ型はCLOB、ストレージはリクエストごと(メモリーのみ)を選択します。
識別の名前はonClick Start AI Assistantとします。ボタンのデフォルトである、クリックのタイミングで実行します。
最初にサーバー側で、質問文に類似したチャンクを取り出しプロンプトとする処理を実行します。
TRUEアクションとしてサーバー側のコードを選択し、設定の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 | |
l_request_json json_object_t; | |
l_request clob; | |
l_texts json_array_t; | |
l_response clob; | |
l_response_json json_object_t; | |
l_embeddings json_array_t; | |
l_embedding json_array_t; | |
e_embed_failed exception; | |
l_embed vector; | |
l_prompt clob; | |
l_norm varchar2(4000); | |
begin | |
/* | |
* Cohere Embed APIに送信するリクエストを生成する。 | |
*/ | |
l_request_json := json_object_t(); | |
l_request_json.put('model', 'embed-multilingual-light-v3.0'); | |
l_request_json.put('input_type', 'search_query'); | |
l_texts := json_array_t(); | |
l_texts.append(:P1_QUESTION); | |
l_request_json.put('texts', l_texts); | |
l_request := l_request_json.to_clob(); | |
/* | |
* Cohere Embed APIを呼び出す。 | |
*/ | |
apex_web_service.clear_request_headers; | |
apex_web_service.set_request_headers('Accept','application/json', p_reset => false); | |
apex_web_service.set_request_headers('Content-Type','application/json', p_reset => false); | |
l_response := apex_web_service.make_rest_request( | |
p_url => 'https://api.cohere.ai/v1/embed' | |
,p_http_method => 'POST' | |
,p_body => l_request | |
,p_credential_static_id => 'COHERE_API_KEY' | |
); | |
if apex_web_service.g_status_code <> 200 then | |
apex_debug.info(l_response); | |
raise e_embed_failed; | |
end if; | |
/* | |
* レスポンスから質問文のベクトルを取り出す。 | |
*/ | |
l_response_json := json_object_t.parse(l_response); | |
l_embeddings := l_response_json.get_array('embeddings'); | |
l_embed := to_vector(l_embeddings.get(0).to_string()); | |
/* | |
* 回答に使う文字列をP1_PROMPTへ保存する。 | |
*/ | |
l_prompt := ''; | |
for r in ( | |
select chunk | |
from ebmj_embeddings | |
order by vector_distance(l_embed, embedding) asc | |
fetch first 2 partitions by url, 2 rows only | |
) | |
loop | |
l_prompt := l_prompt || '######'; | |
l_prompt := l_prompt || apex_application.CRLF; | |
l_norm := r.chunk; | |
-- CRとLFを取り除く。 | |
l_norm := replace(l_norm, chr(10)); | |
l_norm := replace(l_norm, chr(13)); | |
l_norm := replace(l_norm, '\n'); | |
l_prompt := l_prompt || l_norm; | |
l_prompt := l_prompt || apex_application.CRLF; | |
end loop; | |
:P1_PROMPT := l_prompt; | |
end; |
送信するアイテムとして質問文であるP1_QUESTION、戻すアイテムとしてプロンプトであるP1_PROMPTを指定します。実行の初期化時に実行はオフ、結果を待機はオンにします。
TRUEアクションとしてOpen AIアシスタントを作成します。
生成AIのサービスにCohereを選択します。
システム・プロンプトに以下を記述します。
あなたはOracle APEXの専門家です。
この文字列はOpenAIのChat Completions APIの呼び出しではroleがsystemのcontentになります。
"messages": [
{
"role" : "system",
"content" : "ここの文字列"
}
]
システム・プロンプトにはアプリケーション・アイテムやページ・アイテムの置換文字列を含めることができますが、これはページ・レンダリングの前に値が設定されている必要があります。
ようこそメーセージに以下を記述します。
Oracle APEXの質問に答えます。
この文字列はAIアシスタントに表示されるだけで、API呼び出しのメッセージには含まれません。
外観の表示形式はインラインとダイアログを選択できます。ダイアログを選択すると、ダイアログのタイトルが追加の設定項目になります。タイトルをAIチャットとした場合、以下のような画面に変わります。
最初のプロンプトのタイプにはなし、アイテム、JavaScript式のどれかを選択できます。
OpenAIのChat Completions APIの呼び出しでは、roleがsystemのメッセージの直後に追加するroleがuserのcontentを設定しています。
"messages": [
{
"role" : "system",
"content" : "プロンプト"
},
{
"role" : "user",
"content" : "最初のプロンプト"
}
]
表示メッセージとして以下を記述しています。名前の通り表示だけで、API呼び出しには含まれません。
### 前提知識を追加しました。 ###
即時アクション・プロンプトとして&P1_QUESTION.を指定しています。即時アクション・プロンプトの指定があるときは、この値をroleがuserのcontentとしてメッセージに含め、APIを呼び出します。
即時アクション・プロンプトが無指定の場合は、システム・プロンプトと最初のプロンプトは設定されていますが、APIの呼び出しは行われていず、ユーザーの追加入力待ちになります。
この状態のときにクイック・アクションが表示されます。クイック・アクションは2つまで設定できます。
例えば今回の実装で、即時アクション・プロンプトを空白にして、クイック・アクションのメッセージ1に&P1_QUESTION.、メッセージ2に今までの要約をお願いします。を設定します。
この場合、ボタン問い合わせるをクリックすると選択肢としてOracle APEXからGoogleのサービスを呼び出せますか?と今までの要約をお願いします。が表示されます。
どちらかをクリックすると、roleがuserのcontentにクイック・アクションの文字列が追加され、APIが呼び出されます。
最初のプロンプトとしてJavaScript式を選択すると、APIに含めるメッセージと会話として表示するデータを別々に制御できます。
例えば以下を記述します。
{
fullContent: apex.items.P1_PROMPT.value + "###" + apex.items.P1_QUESTION.value,
content: "**" + apex.items.P1_QUESTION.value + "**",
type: "markdown",
commit: true
}
fullContentはAPIに含まれる文字列です。contentがチャット画面に表示される文字列です。typeとしてmarkdownやhtmlを選択できるため、画面上に表示される文字列を修飾できます。commitがtrueの場合は、すぐにAPI呼び出しを行う、falseの場合はAPI呼び出しは行いません。
レスポンスの使用のタイプにアイテム、アイテムにP1_RESP、JavaScript検出式として以下を記述します。
this.fullContent.toLowerCase().includes( "json" ) ? this.fullContent : ""
そのボタンを押すとJavaScript検出式で返された文字列が、ページ・アイテムP1_RESPに設定されます。また、この処理を持ってAIチャットは終了し、チャット画面が初期化されます。
apex.items.P1_RESP.value = this;
今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/apex-chat-sample-ai-assistant.zip
Oracle APEXのアプリケーション作成の参考になれば幸いです。
完