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対応)以下のGIF動画のように、APEXアプリにファイルをアップロードします。ファイルをアップロードする際に、以下の処理を実施しています。
- アップロードされたファイルからOracle TextのAuto Filterを使って文字列を抽出する。
- 抽出した文字列を、それより小さなチャンクに分割する。
- 分割したチャンクごとにCohereのEmbed APIを呼び出してベクトル埋め込みを生成する。
- 生成したベクトル埋め込みをPineconeのインデックスに保存する。
Pineconeのインデックスを確認すると、3つのドキュメントのアップロードにより1726のベクトルが保存されていることがわかります。
質問を行います。アップロードしたファイルの内容を参照します。質問の際には以下の処理を実行します。
- 質問の文字列からベクトル埋め込みを生成する。
- Pineconeのインデックスを質問のベクトル埋め込みで検索し、類似したチャンクをいくつか取り出す。
- 質問と取り出されたチャンクよりプロンプトを作成し、CohereのGenerate APIを呼び出す。
- 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の以下のモデルから、アプリケーションで使用する表を作成します。
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
# 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 |
以下の5つの表を作成しています。
- KB_DOCUMENTS - アップロードしたファイルを列CONTENT(BLOB形式)に保存し、それより抽出した文字列を列CONTENT_TEXT(CLOB形式)に保存する。CONTENTから文字列の抽出に失敗したときは、IS_FAILEDにYを立てる。
- KB_CHUNKS - KB_DOCUMENTSのCONTENT_TEXTの内容をより小さなチャンクに分割し、列CHUNKに保存する。チャンクより生成したベクトル埋め込みを列EMBEDDINGに保存する。ベクトル・データベースに保存済みであれば、IS_INDEXEDにYを立てる。
- KB_QUESTIONS - 入力した質問を列QUESTIONに保存する。質問の文字列から生成したベクトル埋め込みを列EMBEDDINGに保存する。
- KB_ANSWERS - 質問のベクトル埋め込みより、インデックスを検索する。検索結果として返されるID(チャンクのベクトル埋め込みをインデックスに保存する際、KB_CHUNKSのIDをベクトルのIDとしている)とSCOREを列CHUNK_IDとSCOREに保存する。
- KB_RESPONSES - 質問とインデックスの検索結果より得られたチャンクを使ってプロンプトを生成する。生成したプロンプトを列PROMPTに保存し、LLMのAPIを呼び出して得られた回答をGENERATED_ANSWERに保存する。今のところCohereではChat形式のAPIは提供されていないので、ITERATIONは常に1になる。繰り返し問い合わせることで回答をRefineできる場合は、このITERATIONが増える。
このアプリケーションが使用する表の説明は以上です。
パッケージの作成
ほとんどの処理はパッケージKB_LLM_UTILに実装しています。パッケージのコードは以下です。
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 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; | |
/ |
以下の6つのプロシージャを含んでいます。
- APPLY_AUTO_FILTER - ファイルから文字列を抽出する。
- SPLIT_INTO_CHUNKS - 抽出した文字列をチャンクに分割する。
- GENERATE_EMBEDDINGS - チャンクのベクトル埋め込みを生成する。
- UPSERT_VECTORS - チャンクのベクトル埋め込みをベクトル・データベースのインデックスに保存する。
- ASK - 質問を受け付け、インデックスを検索する。質問と検索結果のチャンクより、生成AIのAPIを呼び出し回答を生成する。
- DELETE_VECTORS - 指定した文書に紐づくベクトルをインデックスから削除する。
このプロシージャの中で、検索結果への影響が大きいプロシージャはSPLIT_INTO_CHUNKSとASKです。SPLIT_INTO_CHUNKSにおいて、アップロードされたファイルの内容を、回答として意味があるような塊として分割することは非常に重要です。また、ASKでは、生成AIが適切な回答を返すようなプロンプトを生成する必要があります。
APEXアプリケーションの実装
アプリケーション作成ウィザードを起動し、ホーム・ページを削除し、代わりに表KB_DOCUMENTSをソースとしたフォーム付き対話モード・レポートのページを追加します。
対話モード・レポートのページの追加では、表またはビューとしてKB_DOCUMENTSを選択し、フォームを含めるにチェックを入れます。
表KB_DOCUMETNSにはBLOB型の列CONTENTが含まれます。APEXのアプリケーション作成ウィザード(およびページ作成ウィザード)は、BLOB列へのファイルのアップロード機能を自動で実装します。
ページ番号1の対話モード・レポートでは、CLOB型の列CONTENT_TEXTはデータが大きすぎて表示できません。アクション・メニューの列より、表示対象から除きます。
ページ・デザイナで対話モード・レポートのページを開き、列CONTENTのBLOB属性を設定します。MIMEタイプ列としてCONTENT_MIMETYPE、ファイル名列としてCONTENT_FILENAME、最終更新列としてCONTENT_LASTUPD、文字セット列としてCONTENT_CHARSETを設定します。
これらの列は、クイックSQLのモデルにデータ型としてfileを指定すると、自動的に生成されます。
ファイルの作成、更新、削除と同時に、ベクトル・データベースも更新するようにプロセスを追加します。
プロセス・ビューを開きます。アプリケーション作成ウィザードによって、プロセスプロセス・フォーム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.GENERATE_EMBEDDINGSを呼び出します。
以上で、ファイルをアップロードすると、テキストの抽出、チャンクへの分割、ベクトル埋め込みの生成、インデックスへのUpsertが順次実施されます。
検索ページの作成
空白のページを作成します。ページ・アイテムやリージョンを作成し、以下のように配置します。
ページ・アイテムP3_PROMPT_TYPEは、Cohere Generate APIに渡すpromptの形式を指定します。タイプは選択リストです。
選択肢として、質問のみをQ、質問が後 - スコア昇順をA、質問が前 - スコア降順をBとして定義します。
ページ・アイテムP3_QUESTIONに質問を入力します。タイプはテキスト領域です。
ボタンSUBMITをクリックすると、質問文からのベクトル埋め込みの生成、インデックスの検索、プロンプトの作成とCohere Generate APIの呼び出し、といった一連の処理を実行します。
ページ・アイテムP3_TOP_K、P3_TEMPERATURE、P3_SCORE_LIMIT、P3_MAX_TOKENSのタイプは数値フィールドで、主にAPI呼び出しに指定するパラメータを与えます。
P3_TOP_KはPineconeのquery APIの引数topKに渡されます。ここで指定した数だけ質問文に類似したチャンクが返されます。P3_TEMPERATUREはCohere Generate APIの引数temperatureに渡されます。0から5の間の値になります。低い値であるほど、揺らぎの少ない回答が得られます。P3_SCORE_LIMITはAPIへの引数ではなく、Pineconeのインデックスの検索結果から、これ以下のスコアのチャンクはプロンプトに含めないように制限します。P3_MAX_TOKENSはCohere 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を表示します。回答の元になったプロンプトを確認するために使用します。タイプはテキスト領域です。
パッケージ・プロシージャKB_LLM_UTIL.ASKを呼び出します。パラメータはページ・アイテムに割り当てられているか、または、APIデフォルトです。
APEXアプリケーションの説明は以上になります。
実際にアプリケーションを作成して、いくつか感じたことを列挙しておきます。
- 現状、Cohereで日本語を扱うのは難しい印象。
- LlamaIndexでもデフォルトではResponse Synthesizerは凝ったことはしていない模様。なので、ここ(プロシージャKB_LLM_UTIL.ASK)がカスタマイズのポイントでは無さそう。
- チャンクへの分割が重要。一塊の説明をチャンクにする必要があるが、実装は難しそう。OpenAIのAPI呼び出しでチャンクに分割できれば、それが一番簡単で精度が高いのではないか。
- Oracle TextのAuto Filterの適用は失敗することがあり、IS_FAILED列の確認は必要。
Oracle APEXのアプリケーション作成の参考になれば幸いです。
完