2023年7月19日水曜日

APEXのアプリケーションでIn-context Learningを実装してみる

 OpenAIのChatGPTでIn-context Learningを実装するために、LlamaIndexがよく使われているようです。本ブログでも以前に記事として取り上げたことがあります。

LlamaIndexは色々な外部連携の手段(Data Connectorsや各種のIndexesなど)を提供しています。それらを活用するには、基本Pythonでのコーディングとなり、ほとんどOracle APEXの出番はありません。Oracle APEXでアプリケーションを作る方にPythonが得意な方が多いとは思いませんし、また、SQL、PL/SQL、JavaScript、HTMLにCSSと覚えて、さらにPythonとなると、かなり大変です。

LlamaIndexの仕組みを勉強して、似たような処理を行なうAPEXアプリケーションを作ってみました。以下の記事よりLlamaIndexについて勉強させていただきました。

DeveloperIOの記事より
LlamaIndexを完全に理解するチュートリアル その1:処理の概念や流れを理解する基礎編(v0.6.8対応)

ドキュメントの保存にOracle Databaseを使い、LLMにCohere(ベクトル埋め込みと回答の生成に使用)、ベクトル・ストアとしてPineconeを使用します。

以下のGIF動画のように、APEXアプリにファイルをアップロードします。ファイルをアップロードする際に、以下の処理を実施しています。
  1. アップロードされたファイルからOracle TextのAuto Filterを使って文字列を抽出する。
  2. 抽出した文字列を、それより小さなチャンクに分割する。
  3. 分割したチャンクごとにCohereのEmbed APIを呼び出してベクトル埋め込みを生成する。
  4. 生成したベクトル埋め込みをPineconeのインデックスに保存する。

Pineconeのインデックスを確認すると、3つのドキュメントのアップロードにより1726のベクトルが保存されていることがわかります。


質問を行います。アップロードしたファイルの内容を参照します。質問の際には以下の処理を実行します。
  1. 質問の文字列からベクトル埋め込みを生成する。
  2. Pineconeのインデックスを質問のベクトル埋め込みで検索し、類似したチャンクをいくつか取り出す。
  3. 質問と取り出されたチャンクよりプロンプトを作成し、CohereのGenerate APIを呼び出す。
  4. CohereのGenerate APIの応答を回答とする。

CohereのGenerate APIのリファレンスには(Summarizeと異なり)英語のみとは書かれていません。しかし、日本語による回答は文書になっていないことが多いです。結果として英語でないとAPIの回答を評価できません

Cohereの代わりにOpenAIを使用する場合は、ベクトル埋め込み(embedding)を生成するAPIと、そのモデルとしてtext-embedding-ada-002が使用できます。この場合はインデックスの次元は1536になります。また、回答の生成にはChat Completion APIを使用します。

上記のアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/vector-documents-search.zip

これより、簡単に実装について説明します。


表の作成



クイックSQLの以下のモデルから、アプリケーションで使用する表を作成します。

# prefix: kb
documents
title vc160 /nn
content file
content_text clob
is_failed vc1
chunks
document_id num /fk documents
seq num
chunk vc4000
embedding json
split_char vc1
is_indexed vc1
questions
question vc512
embedding json
asked_by vc80
asked_date date /default sysdate
answers
question_id num /fk questions
chunk_id num /fk chunks
score num
answered_date date /default sysdate
responses
question_id num /fk questions
iteration num
prompt vc4000
generated_answer vc4000
view raw kb.txt hosted with ❤ by GitHub


以下の5つの表を作成しています。
  • KB_DOCUMENTS - アップロードしたファイルを列CONTENT(BLOB形式)に保存し、それより抽出した文字列を列CONTENT_TEXT(CLOB形式)に保存する。CONTENTから文字列の抽出に失敗したときは、IS_FAILEDYを立てる。
  • KB_CHUNKS - KB_DOCUMENTSのCONTENT_TEXTの内容をより小さなチャンクに分割し、列CHUNKに保存する。チャンクより生成したベクトル埋め込みを列EMBEDDINGに保存する。ベクトル・データベースに保存済みであれば、IS_INDEXEDYを立てる。
  • KB_QUESTIONS - 入力した質問を列QUESTIONに保存する。質問の文字列から生成したベクトル埋め込みを列EMBEDDINGに保存する。
  • KB_ANSWERS - 質問のベクトル埋め込みより、インデックスを検索する。検索結果として返されるID(チャンクのベクトル埋め込みをインデックスに保存する際、KB_CHUNKSのIDをベクトルのIDとしている)とSCOREを列CHUNK_IDSCOREに保存する。
  • KB_RESPONSES - 質問とインデックスの検索結果より得られたチャンクを使ってプロンプトを生成する。生成したプロンプトを列PROMPTに保存し、LLMのAPIを呼び出して得られた回答をGENERATED_ANSWERに保存する。今のところCohereではChat形式のAPIは提供されていないので、ITERATIONは常にになる。繰り返し問い合わせることで回答をRefineできる場合は、このITERATIONが増える。
このアプリケーションが使用する表の説明は以上です。


パッケージの作成



ほとんどの処理はパッケージKB_LLM_UTILに実装しています。パッケージのコードは以下です。

create or replace package kb_llm_util as
/**
* 表KB_DOCUMENTSのBLOB列CONTENTより文字を抽出して
* CLOB列CONTENT_TEXTへ書き込む。
* 失敗した場合はIS_FAILEDにYを立てる。
*
* Oracle TextのAUTO_FILTERを呼び出すためのプリファレンスと
* ポリシーの作成をあらかじめ作成しておく。
----
begin
ctx_ddl.create_preference('auto_filter', 'AUTO_FILTER');
ctx_ddl.create_policy('auto_policy', 'auto_filter');
end;
----
*/
procedure apply_auto_filter(
p_id in number
,p_policy_name in varchar2 default 'auto_policy'
);
/**
* 表KB_DOCUMENTSの列CONTENT_TEXTに保存されている文章をチャンクに分割する。
* 分割されたチャンクは表KB_CHUNKSに保存される。
*
* LlamaIndexでいうところのNode Parserに該当する処理を行う。
* https://gpt-index.readthedocs.io/en/latest/core_modules/data_modules/node_parsers/root.html
*
* どのような形でチャンクに分割するかはとても重要。以下では、
* 日本語であることを前提として、句読点の。もしくは、で分割している。
*/
procedure split_into_chunks(
p_id in number
,p_primary_separator in varchar2 default '。'
,p_secondary_separator in varchar2 default '、'
,p_limit in number default 400
);
/**
* 表KB_CHUNKSに保存した列CHUNKのベクトル埋め込みを作成する。
* CohereのEmbedを呼び出す。モデルはembed-multilingual-v2.0。
*
* 生成したベクトル埋め込みは列EMBEDDINGに保存する。
*
* Cohereの無料枠の無料枠には、1分間で100リクエストというレート制限が
* あるため、1回りクエストを発行したら1秒スリープしている。
*/
procedure generate_embeddings(
p_id in number
,p_collection_name in varchar2 default 'EMBEDDINGS'
,p_model_name in varchar2 default 'embed-multilingual-v2.0'
,p_cohere_cred in varchar2 default 'COHERE_API'
);
/**
* 生成したベクトル埋め込みをPineconeのインデックスにUpsertする。
*/
procedure upsert_vectors(
p_id in number
,p_index in varchar2
,p_pinecone_cred in varchar2 default 'PINECONE_API'
);
/**
* 質問の送信と回答の表示。
*
* 質問の文字列のベクトル埋め込みを生成し、Pineconeのインデックスを検索する。
* 回答数はp_top_kで指定する。
*
* LlamaIndexでいうところのResponse Synthesizerに該当する処理を行う。
* https://gpt-index.readthedocs.io/en/latest/core_modules/query_modules/response_synthesizers/root.html
* OpenAIのAPIであれば、LlamaIndexでのpromptは以下。
* https://gpt-index.readthedocs.io/en/latest/core_modules/model_modules/prompts.html
*
* Cohereのgenerate向けのpromptとして良いものは見つからず。とりあえず以下の形式を定義した。
* p_prompt_typeが Q
* 質問文だけをpromptにする。
* p_prompt_typeが A
* インデックス検索の結果をスコアの低い順からpromptに含め、
* 最後に質問文を追記する。
* p_prompt_typeが D
* promptの最初に質問文を記載し、その後に
* インデックス検索の結果をスコアの高い順からプロンプトに含る。
*
* 上記をpromptとしてCohereのgenerateを呼び出し、得られたgenerationを回答とする。
* Cohereのgenerateに日本語で質問できるが、おかしな回答になる。
*/
procedure ask(
p_question in varchar2
,p_top_k in number
,p_prompt_type in varchar2
,p_index in varchar2
,p_answer out varchar2
,p_question_id out number
,p_score_limit in number default 0
,p_model_name in varchar2 default 'embed-multilingual-v2.0'
,p_cohere_cred in varchar2 default 'COHERE_API'
,p_pinecone_cred in varchar2 default 'PINECONE_API'
,p_generate_model_name in varchar2 default 'command'
,p_temperature in number default 0.9
,p_max_tokens in number default 100
);
/**
* 削除された文書のベクトルをPineconeのインデックスから削除する。
*/
procedure delete_vectors(
p_id in number
,p_index in varchar2
,p_pinecone_cred in varchar2 default 'PINECONE_API'
);
end;
/
create or replace package body kb_llm_util as
/**
* Extract text string from BLOB column.
*/
procedure apply_auto_filter(
p_id in number
,p_policy_name in varchar2
)
as
l_content kb_documents.content%type;
l_content_text kb_documents.content_text%type;
l_is_failed kb_documents.is_failed%type := 'N';
begin
select content into l_content from kb_documents where id = p_id;
if dbms_lob.getlength(l_content) > 0 then
begin
/* auto_policy to apply AUTO_FILTER must be created. */
ctx_doc.policy_filter(
policy_name => p_policy_name
, document => l_content
, restab => l_content_text
, plaintext => true);
exception
when others then
/* set Y to IS_FAILED to the row without CONTENT_TEXT */
l_is_failed := 'Y';
end;
end if;
update kb_documents set content_text = l_content_text, is_failed = l_is_failed
where id = p_id;
end apply_auto_filter;
/**
* Split document into chunks.
*/
procedure split_into_chunks(
p_id in number
,p_primary_separator in varchar2
,p_secondary_separator in varchar2
,p_limit in number
)
as
l_content_text kb_documents.content_text%type;
l_chunk kb_chunks.chunk%type;
l_seq kb_chunks.seq%type;
l_split_char kb_chunks.split_char%type;
l_split number;
begin
/* Delete chunks currently exists for update. */
delete from kb_chunks where document_id = p_id;
/* consecutive whitespaces into a single whitespace. */
select regexp_replace(content_text,'\s+',' ') into l_content_text from kb_documents where id = p_id;
l_seq := 1;
while true
loop
l_split_char := p_primary_separator;
l_split := instr(l_content_text, l_split_char);
if l_split > p_limit then
l_split_char := p_secondary_separator;
l_split := instr(l_content_text, l_split_char);
if l_split > p_limit then
/*
* if primary_separator or secondary_separactor
* could not find within p_limit,
* A string of length p_limit become a chunk.
*/
l_split_char := '';
l_split := p_limit;
end if;
end if;
l_chunk := substr(l_content_text, 1, l_split);
if l_chunk is null then
/* entire document has been split. */
exit;
end if;
insert into kb_chunks(document_id, seq, chunk, split_char) values(p_id, l_seq, trim(l_chunk), l_split_char);
l_content_text := substr(l_content_text, (l_split+1));
l_seq := l_seq + 1;
end loop;
end split_into_chunks;
/**
  * Generate each embedding from chunks.
*/
procedure generate_embeddings(
p_id in number
,p_collection_name in varchar2
,p_model_name in varchar2
,p_cohere_cred in varchar2
)
as
l_count number;
l_request clob;
l_request_json json_object_t;
l_texts json_array_t;
l_response clob;
l_response_json json_object_t;
l_embeddings json_array_t;
l_embedding json_array_t;
l_embedding_clob clob;
l_chunk_id kb_chunks.id%type;
e_llm_embed_failed exception;
begin
while true
loop
/* exit if no candidate for generating embedding. */
select count(*) into l_count from kb_chunks where embedding is null and document_id = p_id;
if l_count = 0 then
exit;
end if;
/* select 90 chunks (96 is hard limit) to generate embeddings. */
apex_collection.create_or_truncate_collection(p_collection_name);
l_count := 1;
for r in (
select id, chunk from kb_chunks where embedding is null and document_id = p_id
)
loop
apex_collection.add_member(
p_collection_name => p_collection_name
,p_n001 => r.id
,p_c001 => r.chunk
);
l_count := l_count + 1;
if l_count > 90 then
exit;
end if;
end loop;
apex_collection.resequence_collection(p_collection_name);
/* create a request body for Cohere Embed. */
l_texts := json_array_t();
for r in (
select c001 from apex_collections where collection_name = p_collection_name order by seq_id
)
loop
l_texts.append(r.c001);
end loop;
l_request_json := json_object_t();
l_request_json.put('texts', l_texts);
l_request_json.put('model', p_model_name);
l_request_json.put('truncate','END');
l_request := l_request_json.to_clob;
/* call Cohere Embed */
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 => p_cohere_cred
);
if apex_web_service.g_status_code <> 200 then
apex_debug.info(l_response);
raise e_llm_embed_failed;
end if;
/* update embedding */
l_response_json := json_object_t(l_response);
l_embeddings := l_response_json.get_array('embeddings');
for i in 1..l_embeddings.get_size
loop
l_embedding := json_array_t(l_embeddings.get(i-1));
l_embedding_clob := l_embedding.to_clob;
select n001 into l_chunk_id from apex_collections where collection_name = p_collection_name and seq_id = i;
update kb_chunks set embedding = l_embedding_clob where id = l_chunk_id;
end loop;
/* throttling cohere api call to keep its usage in free */
dbms_session.sleep(1);
end loop;
end generate_embeddings;
/**
* store embeddings in vector database.
*/
procedure upsert_vectors(
p_id in number
,p_index in varchar2
,p_pinecone_cred in varchar2
)
as
l_count number;
l_request clob;
l_request_json json_object_t;
l_vectors json_array_t;
l_vector json_object_t;
l_embedding json_array_t;
l_response clob;
l_response_json json_object_t;
C_UPSERT constant varchar2(100) := p_index || '/vectors/upsert';
e_upsert_vectors_failed exception;
begin
while true
loop
/* exit if all embeddings are stored. */
select count(*) into l_count from kb_chunks
where document_id = p_id and embedding is not null and (is_indexed is null or is_indexed <> 'Y');
if l_count = 0 then
exit;
end if;
/* store 90 embeddings in single upsert request */
l_count := 1;
l_vectors := json_array_t();
for r in (
select id, embedding from kb_chunks
where embedding is not null and (is_indexed is null or is_indexed <> 'Y')
)
loop
l_vector := json_object_t();
l_vector.put('id', to_char(r.id));
l_vector.put('values', json_array_t(r.embedding));
l_vectors.append(l_vector);
update kb_chunks set is_indexed = 'Y' where id = r.id;
l_count := l_count + 1;
if l_count > 90 then
exit;
end if;
end loop;
l_request_json := json_object_t();
l_request_json.put('vectors', l_vectors);
l_request := l_request_json.to_clob;
/*
* call psert api of Pinecone - vectore database
*/
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 => C_UPSERT
,p_http_method => 'POST'
,p_body => l_request
,p_credential_static_id => p_pinecone_cred
);
if apex_web_service.g_status_code <> 200 then
apex_debug.info(l_response);
raise e_upsert_vectors_failed;
end if;
end loop;
end upsert_vectors;
/**
* Qusetion and Answer
*/
procedure ask(
p_question in varchar2
,p_top_k in number
,p_prompt_type in varchar2
,p_index in varchar2
,p_answer out varchar2
,p_question_id out number
,p_score_limit in number
,p_model_name in varchar2
,p_cohere_cred in varchar2
,p_pinecone_cred in varchar2
,p_generate_model_name in varchar2
,p_temperature in number
,p_max_tokens in number
)
as
l_request clob;
l_request_json json_object_t;
l_response clob;
l_response_json json_object_t;
l_embeddings json_array_t;
l_embedding json_array_t;
l_embedding_clob clob;
l_question_id kb_questions.id%type;
/* Pinecone */
l_matches json_array_t;
l_vector json_object_t;
/* search result */
l_chunk_id varchar2(400);
l_score number;
C_QUERY constant varchar2(80) := p_index || '/query';
l_prompt kb_responses.prompt%type;
l_generations json_array_t;
l_generated_answer kb_responses.generated_answer%type;
e_query_vectors_failed exception;
e_llm_embed_failed exception;
e_llm_generate_failed exception;
e_bad_prompt_type exception;
begin
/* generate embedding from question */
select json_object(
key 'texts' value json_array(p_question)
,key 'model' value p_model_name
,key 'truncate' value 'END'
returning clob) into l_request from dual;
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 => p_cohere_cred
);
if apex_web_service.g_status_code <> 200 then
apex_debug.info(l_response);
raise e_llm_embed_failed;
end if;
l_response_json := json_object_t.parse(l_response);
l_embeddings := l_response_json.get_array('embeddings');
l_embedding := json_array_t(l_embeddings.get(0));
l_embedding_clob := l_embedding.to_clob;
/* store question in table KB_QUESTIONS. */
insert into kb_questions(question, embedding) values(p_question, l_embedding_clob)
returning id into l_question_id;
p_question_id := l_question_id;
/* throttring cohere api call */
dbms_session.sleep(1);
/*
* query pinecone by embedding generated from the question.
*/
l_request_json := json_object_t();
l_request_json.put('includeValues', false);
l_request_json.put('includeMetadata', false);
l_request_json.put('vector', l_embedding);
l_request_json.put('topK', p_top_k);
l_request := l_request_json.to_clob;
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 => C_QUERY
,p_http_method => 'POST'
,p_body => l_request
,p_credential_static_id => p_pinecone_cred
);
if apex_web_service.g_status_code <> 200 then
apex_debug.info(l_response);
raise e_query_vectors_failed;
end if;
/* store response from Pinecone in table KB_ANSWERS. */
l_response_json := json_object_t.parse(l_response);
l_matches := l_response_json.get_array('matches');
for i in 1..l_matches.get_size
loop
l_vector := json_object_t(l_matches.get(i-1));
l_chunk_id := l_vector.get_string('id');
l_score := l_vector.get_number('score');
insert into kb_answers(question_id, chunk_id, score) values(l_question_id, l_chunk_id, l_score);
end loop;
/*
* Create Prompt for Cohere generate.
*/
if p_prompt_type = 'Q' then
/* only question for prompt */
l_prompt := p_question;
elsif p_prompt_type = 'A' then
/* low score to high score, question is the last */
l_prompt := '';
for r in (
select c.chunk from kb_answers a join kb_chunks c on a.chunk_id = c.id
where a.question_id = l_question_id and a.score > p_score_limit
order by a.score asc
)
loop
l_prompt := l_prompt || r.chunk || apex_application.LF || apex_application.LF;
end loop;
l_prompt := l_prompt || p_question;
elsif p_prompt_type = 'D' then
/* question is the first, high store to low score */
l_prompt := p_question;
for r in (
select c.chunk from kb_answers a join kb_chunks c on a.chunk_id = c.id
where a.question_id = l_question_id and a.score > p_score_limit
order by a.score desc
)
loop
l_prompt := l_prompt || apex_application.LF || apex_application.LF || r.chunk ;
end loop;
else
apex_debug.info('badc prompt_type = %s', p_prompt_type);
raise e_bad_prompt_type;
end if;
/*
* call Cohere generate with the prompt.
*/
l_request_json := json_object_t();
l_request_json.put('prompt', l_prompt);
l_request_json.put('model', p_generate_model_name);
l_request_json.put('temperature', p_temperature);
l_request_json.put('max_tokens', p_max_tokens);
l_request := l_request_json.to_clob;
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/generate'
,p_http_method => 'POST'
,p_body => l_request
,p_credential_static_id => p_cohere_cred
);
if apex_web_service.g_status_code <> 200 then
apex_debug.info(l_response);
raise e_llm_generate_failed;
end if;
l_response_json := json_object_t(l_response);
l_generations := l_response_json.get_array('generations');
l_generated_answer := json_object_t(l_generations.get(0)).get_string('text');
/*
* store reponse generated by Cohere generate for further review.
* Chat API is not available at this moment so it is not possible to refine
* answer by consecutive request.
* Column ITERATION is added for this purpose but currently always 1.
*/
insert into kb_responses(question_id, iteration, prompt, generated_answer)
values(l_question_id, 1, l_prompt, l_generated_answer);
p_answer := l_generated_answer;
end ask;
/* delete vectors from Pinecone */
procedure delete_vectors(
p_id in number
,p_index in varchar2
,p_pinecone_cred in varchar2
)
as
l_request clob;
l_request_json json_object_t;
l_vectors json_array_t;
l_response clob;
C_DELETE constant varchar2(100) := p_index || '/vectors/delete';
e_delete_vectors_failed exception;
begin
l_vectors := json_array_t();
for r in (select id from kb_chunks where document_id = p_id)
loop
l_vectors.append(to_char(r.id));
end loop;
l_request_json := json_object_t();
l_request_json.put('ids', l_vectors);
l_request_json.put('deleteAll', 'false');
l_request := l_request_json.to_clob;
/*
* Pineconeのdeleteを呼び出す。
*/
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 => C_DELETE
,p_http_method => 'POST'
,p_body => l_request
,p_credential_static_id => p_pinecone_cred
);
if apex_web_service.g_status_code <> 200 then
apex_debug.info(l_response);
raise e_delete_vectors_failed;
end if;
/* delete chuks of the document from kb_chunks. */
delete from kb_chunks where document_id = p_id;
end delete_vectors;
end kb_llm_util;
/
view raw kb_llm_util.sql hosted with ❤ by GitHub
以下の6つのプロシージャを含んでいます。
  • APPLY_AUTO_FILTER - ファイルから文字列を抽出する。
  • SPLIT_INTO_CHUNKS - 抽出した文字列をチャンクに分割する。
  • GENERATE_EMBEDDINGS - チャンクのベクトル埋め込みを生成する。
  • UPSERT_VECTORS - チャンクのベクトル埋め込みをベクトル・データベースのインデックスに保存する。
  • ASK - 質問を受け付け、インデックスを検索する。質問と検索結果のチャンクより、生成AIのAPIを呼び出し回答を生成する。
  • DELETE_VECTORS - 指定した文書に紐づくベクトルをインデックスから削除する。
このプロシージャの中で、検索結果への影響が大きいプロシージャはSPLIT_INTO_CHUNKSASKです。SPLIT_INTO_CHUNKSにおいて、アップロードされたファイルの内容を、回答として意味があるような塊として分割することは非常に重要です。また、ASKでは、生成AIが適切な回答を返すようなプロンプトを生成する必要があります。


APEXアプリケーションの実装



CohereのAPIキーの取得とWeb資格証明の作成については、こちらの記事が参考になります。Pineconeについては、こちらの記事が参考になります。

アプリケーション作成ウィザードを起動し、ホーム・ページを削除し、代わりに表KB_DOCUMENTSをソースとしたフォーム付き対話モード・レポートのページを追加します。


対話モード・レポートページの追加では、表またはビューとしてKB_DOCUMENTSを選択し、フォームを含めるチェックを入れます。


表KB_DOCUMETNSにはBLOB型の列CONTENTが含まれます。APEXのアプリケーション作成ウィザード(およびページ作成ウィザード)は、BLOB列へのファイルのアップロード機能を自動で実装します。

ページ番号1の対話モード・レポートでは、CLOB型の列CONTENT_TEXTはデータが大きすぎて表示できません。アクション・メニューの列より、表示対象から除きます。


ページ・デザイナで対話モード・レポートのページを開き、列CONTENTBLOB属性を設定します。MIMEタイプ列としてCONTENT_MIMETYPE、ファイル名列としてCONTENT_FILENAME最終更新列としてCONTENT_LASTUPD文字セット列としてCONTENT_CHARSETを設定します。

これらの列は、クイックSQLのモデルにデータ型としてfileを指定すると、自動的に生成されます。


ページ・デザイナでフォームのページを開き、BLOB列に対応するページ・アイテムP2_CONTENT設定を追加します。MIMEタイプ列としてP2_CONTENT_MIMETYPEファイル名列としてP2_CONTENT_FILENAME文字セット列としてP2_CONTENT_CHARSETBLOB最終更新列としてP2_CONTENT_LASTUPDを指定します。


ファイルの作成、更新、削除と同時に、ベクトル・データベースも更新するようにプロセスを追加します。

プロセス・ビューを開きます。アプリケーション作成ウィザードによって、プロセスプロセス・フォームDocumentダイアログを閉じるが作成されています。


プロセスベクトルの削除は、プロレス・フォームDocumentより上の配置します。サーバー側の条件タイプリクエストは値に含まれるを選択し、SAVE,DELETEを指定します。ファイルの更新(変更の適用をクリックしたとき)では、既存のファイルより生成されたベクトルを最初に削除した上で、新規ファイルと同じ手順で作成されたベクトルで置き換えます。

パッケージ・プロシージャKB_LLM_UTIL.DELETE_VECTORSを呼び出します。

パラメータp_idには、表KB_DOCUMENTSの主キーであるページ・アイテムP3_IDが設定されます。パラメータp_indexには置換文字列G_INDEX、p_pinecone_credはPineconeにアクセスするためのWeb資格証明を設定します。


ファイルのアップロード後に実行されるベクトル・データベースへのベクトルの保存を行なう一連のプロセスは、実行チェーンを使ってまとめます。

識別名前索引作成タイプ実行チェーンを選択します。設定バックグラウンドの実行オフにします。一連の処理は時間がかかるため、設定のバックグラウンド実行をオンに変えるとブラウザの反応が良くなります。ただし、CohereのAPIの無料枠に設けられているレート制限にかかる可能性があります。

プロセステキストの抽出では、パッケージ・プロシージャKB_LLM_UTIL.APPLY_AUTO_FILTERを呼び出します。


プロセスチャンク分割では、パッケージ・プロシージャKB_LLM_UTIL.SPLIT_INTO_CHUNKSを呼び出します。


プロセスベクトル埋め込みの生成では、パッケージ・プロシージャKB_LLM_UTIL.GENERATE_EMBEDDINGSを呼び出します。


プロセスインデックスへのUpsertでは、パッケージ・プロシージャKB_LLM_UTIL.UPSERT_VECTORSを呼び出します。


以上で、ファイルをアップロードすると、テキストの抽出、チャンクへの分割、ベクトル埋め込みの生成、インデックスへのUpsertが順次実施されます。


検索ページの作成



空白のページを作成します。ページ・アイテムやリージョンを作成し、以下のように配置します。


ページ・アイテムP3_PROMPT_TYPEは、Cohere Generate APIに渡すpromptの形式を指定します。タイプ選択リストです。


選択肢として、質問のみQ質問が後 - スコア昇順A質問が前 - スコア降順Bとして定義します。


ページ・アイテムP3_QUESTIONに質問を入力します。タイプテキスト領域です。


ボタンSUBMITをクリックすると、質問文からのベクトル埋め込みの生成、インデックスの検索、プロンプトの作成とCohere Generate APIの呼び出し、といった一連の処理を実行します。


ページ・アイテムP3_ANSWERに、Cohere Generate APIによって生成された回答文を表示します。タイプテキスト領域です。


ページ・アイテムP3_TOP_KP3_TEMPERATUREP3_SCORE_LIMITP3_MAX_TOKENSタイプ数値フィールドで、主にAPI呼び出しに指定するパラメータを与えます。

P3_TOP_KPineconeのquery APIの引数topKに渡されます。ここで指定した数だけ質問文に類似したチャンクが返されます。P3_TEMPERATURECohere Generate APIの引数temperatureに渡されます。0から5の間の値になります。低い値であるほど、揺らぎの少ない回答が得られます。P3_SCORE_LIMITはAPIへの引数ではなく、Pineconeのインデックスの検索結果から、これ以下のスコアのチャンクはプロンプトに含めないように制限します。P3_MAX_TOKENSCohere Generate APIの引数max_tokensに渡されます。


リージョンanswerは、Pineconeのインデックスの検索結果を表示する対話モード・レポートです。ソースSQL問合わせは以下になります。
select a.ID,
       a.QUESTION_ID,
       d.TITLE,
       c.CHUNK,
       a.SCORE,
       a.ANSWERED_DATE
from KB_ANSWERS a 
    join KB_CHUNKS c on a.chunk_id = c.id
    join KB_DOCUMENTS d on d.id = c.document_id
where a.question_id = :P3_QUESTION_ID

ページ・アイテムP3_QUESTION_IDは、質問を保持する表KB_QUESTIONSの主キーであるQUESTION_IDの値を保持します。タイプ非表示です。


ページ・アイテムP3_PROMPTは、Cohere Generate APIに渡されたpromptを表示します。回答の元になったプロンプトを確認するために使用します。タイプテキスト領域です。


ボタンSUBMITを押したときに実行される処理は、プロセス質問の回答として作成します。

パッケージ・プロシージャKB_LLM_UTIL.ASKを呼び出します。パラメータページ・アイテムに割り当てられているか、または、APIデフォルトです。


APEXアプリケーションの説明は以上になります。

実際にアプリケーションを作成して、いくつか感じたことを列挙しておきます。
  1. 現状、Cohereで日本語を扱うのは難しい印象。
  2. LlamaIndexでもデフォルトではResponse Synthesizerは凝ったことはしていない模様。なので、ここ(プロシージャKB_LLM_UTIL.ASK)がカスタマイズのポイントでは無さそう。
  3. チャンクへの分割が重要。一塊の説明をチャンクにする必要があるが、実装は難しそう。OpenAIのAPI呼び出しでチャンクに分割できれば、それが一番簡単で精度が高いのではないか。
  4. Oracle TextのAuto Filterの適用は失敗することがあり、IS_FAILED列の確認は必要。
Oracle APEXのアプリケーション作成の参考になれば幸いです。