langchainrbのコードを読んでいく

手始めに Langchain::LLM::OpenAI を見ていく。
- embed
- complete
- chat
- summarize
が存在する。どれも継承元の LLM::OpenAI::Base にて宣言されているもの。
chatをまず見ていく。
chat(prompt: "", messages: [], context: "", examples: [], **options)
prompt: はユーザーの質問。文字列。
OpenAI::Client.chatに渡されるパラメータは以下をデフォルトとし、optionsで指定されたもので上書きし、プロンプトはprompt, messages, context, examplesを元に構築する形。
DEFAULTS = {
temperature: 0.0,
completion_model_name: "gpt-3.5-turbo",
chat_completion_model_name: "gpt-3.5-turbo",
embeddings_model_name: "text-embedding-ada-002",
dimension: 1536
}.freeze
また、functionsが事前に指定されていれば Function calling が設定される。

.chatのプロンプトの構築方法
指定されたcontextが存在する場合、messagesおよびexamplesからrole: systemのメッセージを削除。
その上で、historyの最初にcontextをシステムプロンプトとして設定する。
historyの最後にpromptをrole: userで追加する。もしpromptが存在せず、historyの最後が{role: user}であれば、historyの最後のメッセージのコンテンツにpromptを付加。
例:
ruby
openai.chat(prompt: "factory_botはいつリリースされましたか?",
context: "質問に答えてください",
messages: [{role: "user", content: "魚は英語で?"}, {role: "assistant", content: "fish"}],
examples: [{role: "user", content: "Ruby on Railsはいつリリースされましたか?"}, {role: "assistant", content: "2004"}],
temperature: 0.3))
上記のコードは内部で以下のようにOpenAI APIを呼び出すことになる。
openai.chat(parameters: {
temperature: 0.3,
completion_model_name: "gpt-3.5-turbo",
chat_completion_model_name: "gpt-3.5-turbo",
embeddings_model_name: "text-embedding-ada-002",
dimension: 1536,
messages: [
{role: 'system', content: '質問に答えてください'},
{role: "user", content: "Ruby on Railsはいつリリースされましたか?"},
{role: "assistant", content: "2004"},
{role: "user", content: "魚は英語で?"},
{role: "assistant", content: "fish"},
{role: 'user', content: 'factory_botはいつリリースされましたか?'}
]
})

summarize(text:)
{role: 'user', content: "Write a concise summary of the following: #{text}"}
で opanai.chat に問い合わせ、結果をstringで返すだけ。あまり捻りはない。
ただ、ここで Langchain::Prompt が利用されている。
prompt_template = Langchain::Prompt.load_from_path(
file_path: Langchain.root.join("langchain/llm/prompts/summarize_template.yaml")
)
prompt = prompt_template.format(text: text)
次はこのLangchain::Promptの実装を見ていく

プロンプトテンプレート
- Langchain::Prompt::PromptTemplate
- Langchain::Prompt::FewShotPromptTemplate
この2種がある模様。

コードを読んでいたら FewShotPromptTemplate のvalidationが動いていないようだったので修正のプルリクエストを投げてみた

Langchain::Prompt::PromptTemplate の機能
基本的にはこの機能のみ。
prompt = Langchain::Prompt::PromptTemplate.new(
template: "Tell me a {adjective} joke.",
input_variables: ["adjective"])
prompt.format(adjective: "funny") # "Tell me a funny joke."
{}でキーワードを囲った形式のテンプレートを作成し、それにキーワードを渡して完成した文を取得できる。

Langchain::Prompt::FewShotPromptTemplate
- 前置き
- 複数の例示
- 最後の文
という形式のプロンプトを作成したいときに利用できる
prompt = Langchain::Prompt::FewShotPromptTemplate.new(
prefix: "Write antonyms for the following words.",
suffix: "Input: {adjective}\nOutput:",
example_prompt: Langchain::Prompt::PromptTemplate.new(
input_variables: ["input", "output"],
template: "Input: {input}\nOutput: {output}"
),
examples: [
{input: "happy", output: "sad"},
{input: "tall", output: "short"}
],
input_variables: ["adjective"]
)
prompt.format(adjective: "good") # ↓になる
# Write antonyms for the following words.
#
# Input: happy
# Output: sad
#
# Input: tall
# Output: short
#
# Input: good
# Output:

examples/pdf_store_and_query_with_chroma.rb
読んでいく。
dotenvの読み込みが無かったのでプルリクを出しておく。
最初のコード
chroma = Langchain::Vectorsearch::Chroma.new(
url: ENV["CHROMA_URL"],
index_name: "documents",
llm: Langchain::LLM::OpenAI.new(api_key: ENV["OPENAI_API_KEY"])
)
chromaのインスタンスを作成する。chromaのサーバーは別で立てておく必要がある。chromaをcloneしてdockerで立てる
git clone git@github.com:chroma-core/chroma.git
cd chroma
docker compose up
デフォルトでは8000番で立つ。
.envのCHROMA_URLを修正する
CHROMA_URL=http://localhost:8000
これでここは通る。

chroma.create_default_schema
このコードは2度目はエラーを返す
Collection documents already exists. (Chroma::InvalidRequestError)
ので、そのときは無視するように修正しておく
begin
chroma.create_default_schema
rescue Chroma::InvalidRequestError
# Ignore this error, it just means the schema already exists
end

# Set up an array of PDF and TXT documents
docs = [
Langchain.root.join("/docs/document.pdf"),
Langchain.root.join("/docs/document.txt"),
Langchain.root.join("/docs/document.docx")
]
# Add data to the index. Weaviate will use OpenAI to generate embeddings behind the scene.
chroma.add_data(
paths: docs
)
これもそのままでは動かない。こんなファイルはプロジェクトに存在しない。
Langchain.rootは langchaing/lib/ を指す Pathlib を返すだけ。
add_dataの実装は以下のようになっている。
def add_data(paths:)
raise ArgumentError, "Paths must be provided" if Array(paths).empty?
texts = Array(paths)
.flatten
.map do |path|
data = Langchain::Loader.new(path)&.load&.chunks
data.map { |chunk| chunk[:text] }
end
texts.flatten!
add_texts(texts: texts)
end
そんで Loader.load の実装を見るとこれは url でもいいようなので、適当なネット上のファイルのURLを渡すように書き換える。
docxもいけるようなので、このリポジトリのテスト用fixtureのtxtのファイルのURLを貼る
# Set up an array of PDF and TXT documents
docs = [
"https://raw.githubusercontent.com/andreibondarev/langchainrb/main/spec/fixtures/loaders/example.txt",
]
lib/langchain/processors/*.rb にある形式には対応していそう
ls lib/langchain/processors
base.rb csv.rb docx.rb html.rb json.rb jsonl.rb pdf.rb text.rb xlsx.rb
だが、これだとadd_textsが通らない。どうするか。

Gemfile.lockにあるchroma-dbのバージョン(0.3.0)だと最新のchroma dbで動かなそうな感じだ。
chroma-db (0.3.0)
Requirements
Ruby 2.7.8 or newer
Chroma Database 0.3.25 or later running as a client/server model.
For Chroma database 0.3.22 or older, please use version 0.3.0 of this gem.
またプルリクを投げるか。

chroma-dbの最新版のバージョンは 0.6.0 。バージョンをここまで上げると関連するテストがガッツリ落ちるので、プルリクは今日はいいや。。。

とりあえず、これで最後までエラーなく実行できるようになる。
res = chroma.ask(
question: "What is Lorem Ipsum?"
)
puts res
# Lorem Ipsum is a placeholder text that is commonly used in the printing and typesetting industry. It has been used since the 1500s as a standard dummy text for testing and demonstrating the visual effects of different fonts, layouts, and designs.
初期状態だと何も出力されないので、putsするようにする。

chroma.add_data では
- 受け取ったURLかPathを拡張子やContent-Typeで対応するLoaderに渡す
- Loaderでデータをパースしてテキストに変換する
- テキストに対してllm(このexampleではOpenAI)のembedを行う
- chromaにembeddingを登録する
といったことを行なっている。

chroma-db アップデートのプルリクを出してmergeされた