langchainrb 0.6.3 → 0.6.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73f980d6a7dd67d0112038a8266a05f8b5697e05c98e61a94598d38406de7c8b
4
- data.tar.gz: 8abc93ad6da8ad05d76ac35eff9aaab963c33549acb94bda4dd83daddeb71f4d
3
+ metadata.gz: 3404535e036c3efe68fd12706d2ebb269caed87b562fc38434122b1be01a356d
4
+ data.tar.gz: e3be77b32cf754235e8895fb1af60edca54cb5acb84278bfa2e39b6ed7c2abbe
5
5
  SHA512:
6
- metadata.gz: 7b5450e51ee732a1e2414e3db5f8a46d113d0b537b561f95556756e2854c9bb9175c898388acc2bb8672b2479e647625d3166580b7b1b25eb6cdc86ff6d42aee
7
- data.tar.gz: b5843004533f952782946e6a753aa5306c6ad4a5f97887416f8f10f4192ca1f88d00d30624cd62581022314649e5d291d9c1ab46f2bab31f9455860fc533c83d
6
+ metadata.gz: b3fae04c73176c758c2d2d32c3ac538f3e094eb10f378b9a8befbbdcc62b60e55941a1bfefcb61eac7daca43ef91d0e57306dbc26bd59afbdad6ab4efff2ba89
7
+ data.tar.gz: 626bb4a226112ee6fe709077a6d49ba91c0483fee657848153e9cff61693183709aede5844237c24cc02c561f59be82ea1fd296fe1c3f4ee4d971494ee4dcd75
data/.env.example CHANGED
@@ -1,10 +1,13 @@
1
1
  AI21_API_KEY=
2
2
  CHROMA_URL=
3
3
  COHERE_API_KEY=
4
+ GOOGLE_PALM_API_KEY=
4
5
  HUGGING_FACE_API_KEY=
6
+ LLAMACPP_MODEL_PATH=
7
+ LLAMACPP_N_THREADS=
8
+ LLAMACPP_N_GPU_LAYERS=
5
9
  MILVUS_URL=
6
10
  OPENAI_API_KEY=
7
- GOOGLE_PALM_API_KEY=
8
11
  OPEN_WEATHER_API_KEY=
9
12
  PINECONE_API_KEY=
10
13
  PINECONE_ENVIRONMENT=
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.4] - 2023-07-01
4
+ - Fix `Langchain::Vectorsearch::Qdrant#add_texts()`
5
+ - Introduce `ConversationMemory`
6
+ - Allow loading multiple files from a directory
7
+ - Add `get_default_schema()`, `create_default_schema()`, `destroy_default_schema()` missing methods to `Langchain::Vectorsearch::*` classes
8
+
3
9
  ## [0.6.3] - 2023-06-25
4
10
  - Add #destroy_default_schema() to Langchain::Vectorsearch::* classes
5
11
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- langchainrb (0.6.3)
4
+ langchainrb (0.6.5)
5
5
  baran (~> 0.1.6)
6
6
  colorize (~> 0.8.1)
7
7
  json-schema (~> 4.0.0)
@@ -153,6 +153,7 @@ GEM
153
153
  addressable (>= 2.8)
154
154
  language_server-protocol (3.17.0.3)
155
155
  lint_roller (1.0.0)
156
+ llama_cpp (0.3.0)
156
157
  loofah (2.21.1)
157
158
  crass (~> 1.0.2)
158
159
  nokogiri (>= 1.5.9)
@@ -327,6 +328,7 @@ DEPENDENCIES
327
328
  hnswlib (~> 0.8.1)
328
329
  hugging-face (~> 0.3.4)
329
330
  langchainrb!
331
+ llama_cpp
330
332
  milvus (~> 0.9.0)
331
333
  nokogiri (~> 1.13)
332
334
  open-weather-ruby-client (~> 0.3.0)
data/README.md CHANGED
@@ -274,6 +274,106 @@ prompt = Langchain::Prompt.load_from_path(file_path: "spec/fixtures/prompt/promp
274
274
  prompt.input_variables #=> ["adjective", "content"]
275
275
  ```
276
276
 
277
+ ### Using Output Parsers
278
+
279
+ Parse LLM text responses into structured output, such as JSON.
280
+
281
+ #### Structured Output Parser
282
+
283
+ You can use the `StructuredOutputParser` to generate a prompt that instructs the LLM to provide a JSON response adhering to a specific JSON schema:
284
+
285
+ ```ruby
286
+ json_schema = {
287
+ type: "object",
288
+ properties: {
289
+ name: {
290
+ type: "string",
291
+ description: "Persons name"
292
+ },
293
+ age: {
294
+ type: "number",
295
+ description: "Persons age"
296
+ },
297
+ interests: {
298
+ type: "array",
299
+ items: {
300
+ type: "object",
301
+ properties: {
302
+ interest: {
303
+ type: "string",
304
+ description: "A topic of interest"
305
+ },
306
+ levelOfInterest: {
307
+ type: "number",
308
+ description: "A value between 0 and 100 of how interested the person is in this interest"
309
+ }
310
+ },
311
+ required: ["interest", "levelOfInterest"],
312
+ additionalProperties: false
313
+ },
314
+ minItems: 1,
315
+ maxItems: 3,
316
+ description: "A list of the person's interests"
317
+ }
318
+ },
319
+ required: ["name", "age", "interests"],
320
+ additionalProperties: false
321
+ }
322
+ parser = Langchain::OutputParsers::StructuredOutputParser.from_json_schema(json_schema)
323
+ prompt = Langchain::Prompt::PromptTemplate.new(template: "Generate details of a fictional character.\n{format_instructions}\nCharacter description: {description}", input_variables: ["description", "format_instructions"])
324
+ prompt_text = prompt.format(description: "Korean chemistry student", format_instructions: parser.get_format_instructions)
325
+ # Generate details of a fictional character.
326
+ # You must format your output as a JSON value that adheres to a given "JSON Schema" instance.
327
+ # ...
328
+ ```
329
+
330
+ Then parse the llm response:
331
+
332
+ ```ruby
333
+ llm = Langchain::LLM::OpenAI.new(api_key: ENV["OPENAI_API_KEY"])
334
+ llm_response = llm.chat(prompt: prompt_text)
335
+ parser.parse(llm_response)
336
+ # {
337
+ # "name" => "Kim Ji-hyun",
338
+ # "age" => 22,
339
+ # "interests" => [
340
+ # {
341
+ # "interest" => "Organic Chemistry",
342
+ # "levelOfInterest" => 85
343
+ # },
344
+ # ...
345
+ # ]
346
+ # }
347
+ ```
348
+
349
+ If the parser fails to parse the LLM response, you can use the `OutputFixingParser`. It sends an error message, prior output, and the original prompt text to the LLM, asking for a "fixed" response:
350
+
351
+ ```ruby
352
+ begin
353
+ parser.parse(llm_response)
354
+ rescue Langchain::OutputParsers::OutputParserException => e
355
+ fix_parser = Langchain::OutputParsers::OutputFixingParser.from_llm(
356
+ llm: llm,
357
+ parser: parser
358
+ )
359
+ fix_parser.parse(llm_response)
360
+ end
361
+ ```
362
+
363
+ Alternatively, if you don't need to handle the `OutputParserException`, you can simplify the code:
364
+
365
+ ```ruby
366
+ # we already have the `OutputFixingParser`:
367
+ # parser = Langchain::OutputParsers::StructuredOutputParser.from_json_schema(json_schema)
368
+ fix_parser = Langchain::OutputParsers::OutputFixingParser.from_llm(
369
+ llm: llm,
370
+ parser: parser
371
+ )
372
+ fix_parser.parse(llm_response)
373
+ ```
374
+
375
+ See [here](https://github.com/andreibondarev/langchainrb/tree/main/examples/create_and_manage_prompt_templates_using_structured_output_parser.rb) for a concrete example
376
+
277
377
  ### Using Agents 🤖
278
378
  Agents are semi-autonomous bots that can respond to user questions and use available to them Tools to provide informed replies. They break down problems into series of steps and define Actions (and Action Inputs) along the way that are executed and fed back to them as additional information. Once an Agent decides that it has the Final Answer it responds with it.
279
379
 
@@ -0,0 +1,52 @@
1
+ require "langchain"
2
+ require "reline"
3
+
4
+ # gem install reline
5
+ # or add `gem "reline"` to your Gemfile
6
+
7
+ openai = Langchain::LLM::OpenAI.new(api_key: ENV["OPENAI_API_KEY"])
8
+
9
+ chat = Langchain::Conversation.new(llm: openai)
10
+ chat.set_context("You are a chatbot from the future")
11
+
12
+ DONE = %w[done end eof exit].freeze
13
+
14
+ puts "Welcome to the chatbot from the future!"
15
+
16
+ def prompt_for_message
17
+ puts "(multiline input; type 'end' on its own line when done. or exit to exit)"
18
+
19
+ user_message = Reline.readmultiline("Question: ", true) do |multiline_input|
20
+ last = multiline_input.split.last
21
+ DONE.include?(last)
22
+ end
23
+
24
+ return :noop unless user_message
25
+
26
+ lines = user_message.split("\n")
27
+ if lines.size > 1 && DONE.include?(lines.last)
28
+ # remove the "done" from the message
29
+ user_message = lines[0..-2].join("\n")
30
+ end
31
+
32
+ return :exit if DONE.include?(user_message.downcase)
33
+
34
+ user_message
35
+ end
36
+
37
+ begin
38
+ loop do
39
+ user_message = prompt_for_message
40
+
41
+ case user_message
42
+ when :noop
43
+ next
44
+ when :exit
45
+ break
46
+ end
47
+
48
+ puts chat.message(user_message)
49
+ end
50
+ rescue Interrupt
51
+ exit 0
52
+ end
@@ -58,6 +58,11 @@ prompt.format(description: "Korean chemistry student", format_instructions: pars
58
58
 
59
59
  # Character description: Korean chemistry student
60
60
 
61
+ llm = Langchain::LLM::OpenAI.new(api_key: ENV["OPENAI_API_KEY"])
62
+ # llm_response = llm.chat(
63
+ # prompt: prompt.format(description: "Korean chemistry student", format_instructions: parser.get_format_instructions)
64
+ # )
65
+
61
66
  # LLM example response:
62
67
  llm_example_response = <<~RESPONSE
63
68
  Here is your character:
@@ -83,7 +88,14 @@ llm_example_response = <<~RESPONSE
83
88
  ```
84
89
  RESPONSE
85
90
 
86
- parser.parse(llm_example_response)
91
+ fix_parser = Langchain::OutputParsers::OutputFixingParser.from_llm(
92
+ llm: llm,
93
+ parser: parser
94
+ )
95
+ # The OutputFixingParser wraps the StructuredOutputParser such that if initial
96
+ # LLM response does not conform to the schema, will call out the LLM to fix
97
+ # the error
98
+ fix_parser.parse(llm_example_response)
87
99
  # {
88
100
  # "name" => "Kim Ji-hyun",
89
101
  # "age" => 22,
@@ -0,0 +1,24 @@
1
+ require "langchain"
2
+
3
+ llm = Langchain::LLM::LlamaCpp.new(
4
+ model_path: ENV["LLAMACPP_MODEL_PATH"],
5
+ n_gpu_layers: Integer(ENV["LLAMACPP_N_GPU_LAYERS"]),
6
+ n_threads: Integer(ENV["LLAMACPP_N_THREADS"])
7
+ )
8
+
9
+ instructions = [
10
+ "Tell me about the creator of Ruby",
11
+ "Write a story about a pony who goes to the store to buy some apples."
12
+ ]
13
+
14
+ template = Langchain::Prompt::PromptTemplate.new(
15
+ template: "{instruction}\n\n### Response:",
16
+ input_variables: %w[instruction]
17
+ )
18
+
19
+ instructions.each do |instruction|
20
+ puts "USER: #{instruction}"
21
+ prompt = template.format(instruction: instruction)
22
+ response = llm.complete prompt: prompt, n_predict: 1024
23
+ puts "ASSISTANT: #{response}"
24
+ end
@@ -17,10 +17,7 @@ module Langchain
17
17
  # end
18
18
  #
19
19
  class Conversation
20
- attr_reader :context, :examples, :messages
21
-
22
- # The least number of tokens we want to be under the limit by
23
- TOKEN_LEEWAY = 20
20
+ attr_reader :options
24
21
 
25
22
  # Intialize Conversation with a LLM
26
23
  #
@@ -31,7 +28,11 @@ module Langchain
31
28
  @llm = llm
32
29
  @context = nil
33
30
  @examples = []
34
- @messages = options.delete(:messages) || []
31
+ @memory = ConversationMemory.new(
32
+ llm: llm,
33
+ messages: options.delete(:messages) || [],
34
+ strategy: options.delete(:memory_strategy)
35
+ )
35
36
  @options = options
36
37
  @block = block
37
38
  end
@@ -39,59 +40,50 @@ module Langchain
39
40
  # Set the context of the conversation. Usually used to set the model's persona.
40
41
  # @param message [String] The context of the conversation
41
42
  def set_context(message)
42
- @context = message
43
+ @memory.set_context message
43
44
  end
44
45
 
45
46
  # Add examples to the conversation. Used to give the model a sense of the conversation.
46
47
  # @param examples [Array<Hash>] The examples to add to the conversation
47
48
  def add_examples(examples)
48
- @examples.concat examples
49
+ @memory.add_examples examples
49
50
  end
50
51
 
51
52
  # Message the model with a prompt and return the response.
52
53
  # @param message [String] The prompt to message the model with
53
54
  # @return [String] The response from the model
54
55
  def message(message)
55
- append_user_message(message)
56
+ @memory.append_user_message(message)
56
57
  response = llm_response(message)
57
- append_ai_message(response)
58
+ @memory.append_ai_message(response)
58
59
  response
59
60
  end
60
61
 
61
- private
62
-
63
- def llm_response(prompt)
64
- @llm.chat(messages: @messages, context: @context, examples: @examples, **@options, &@block)
65
- rescue Langchain::Utils::TokenLength::TokenLimitExceeded => exception
66
- raise exception if @messages.size == 1
67
-
68
- reduce_messages(exception.token_overflow)
69
- retry
62
+ # Messages from conversation memory
63
+ # @return [Array<Hash>] The messages from the conversation memory
64
+ def messages
65
+ @memory.messages
70
66
  end
71
67
 
72
- def reduce_messages(token_overflow)
73
- @messages = @messages.drop_while do |message|
74
- proceed = token_overflow > -TOKEN_LEEWAY
75
- token_overflow -= token_length(message.to_json, model_name, llm: @llm)
76
-
77
- proceed
78
- end
68
+ # Context from conversation memory
69
+ # @return [String] Context from conversation memory
70
+ def context
71
+ @memory.context
79
72
  end
80
73
 
81
- def append_ai_message(message)
82
- @messages << {role: "ai", content: message}
74
+ # Examples from conversation memory
75
+ # @return [Array<Hash>] Examples from the conversation memory
76
+ def examples
77
+ @memory.examples
83
78
  end
84
79
 
85
- def append_user_message(message)
86
- @messages << {role: "user", content: message}
87
- end
88
-
89
- def model_name
90
- @options[:model] || @llm.class::DEFAULTS[:chat_completion_model_name]
91
- end
80
+ private
92
81
 
93
- def token_length(content, model_name, options)
94
- @llm.class::LENGTH_VALIDATOR.token_length(content, model_name, options)
82
+ def llm_response(prompt)
83
+ @llm.chat(messages: @memory.messages, context: @memory.context, examples: @memory.examples, **@options, &@block)
84
+ rescue Langchain::Utils::TokenLength::TokenLimitExceeded => exception
85
+ @memory.reduce_messages(exception)
86
+ retry
95
87
  end
96
88
  end
97
89
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langchain
4
+ class ConversationMemory
5
+ attr_reader :examples, :messages
6
+
7
+ # The least number of tokens we want to be under the limit by
8
+ TOKEN_LEEWAY = 20
9
+
10
+ def initialize(llm:, messages: [], **options)
11
+ @llm = llm
12
+ @context = nil
13
+ @summary = nil
14
+ @examples = []
15
+ @messages = messages
16
+ @strategy = options.delete(:strategy) || :truncate
17
+ @options = options
18
+ end
19
+
20
+ def set_context(message)
21
+ @context = message
22
+ end
23
+
24
+ def add_examples(examples)
25
+ @examples.concat examples
26
+ end
27
+
28
+ def append_ai_message(message)
29
+ @messages << {role: "ai", content: message}
30
+ end
31
+
32
+ def append_user_message(message)
33
+ @messages << {role: "user", content: message}
34
+ end
35
+
36
+ def reduce_messages(exception)
37
+ case @strategy
38
+ when :truncate
39
+ truncate_messages(exception)
40
+ when :summarize
41
+ summarize_messages
42
+ else
43
+ raise "Unknown strategy: #{@options[:strategy]}"
44
+ end
45
+ end
46
+
47
+ def context
48
+ return if @context.nil? && @summary.nil?
49
+
50
+ [@context, @summary].compact.join("\n")
51
+ end
52
+
53
+ private
54
+
55
+ def truncate_messages(exception)
56
+ raise exception if @messages.size == 1
57
+
58
+ token_overflow = exception.token_overflow
59
+
60
+ @messages = @messages.drop_while do |message|
61
+ proceed = token_overflow > -TOKEN_LEEWAY
62
+ token_overflow -= token_length(message.to_json, model_name, llm: @llm)
63
+
64
+ proceed
65
+ end
66
+ end
67
+
68
+ def summarize_messages
69
+ history = [@summary, @messages.to_json].compact.join("\n")
70
+ partitions = [history[0, history.size / 2], history[history.size / 2, history.size]]
71
+
72
+ @summary = partitions.map { |messages| @llm.summarize(text: messages.to_json) }.join("\n")
73
+
74
+ @messages = [@messages.last]
75
+ end
76
+
77
+ def partition_messages
78
+ end
79
+
80
+ def model_name
81
+ @llm.class::DEFAULTS[:chat_completion_model_name]
82
+ end
83
+
84
+ def token_length(content, model_name, options)
85
+ @llm.class::LENGTH_VALIDATOR.token_length(content, model_name, options)
86
+ end
87
+ end
88
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Langchain::LLM
4
+ class ApiError < StandardError; end
5
+
4
6
  # A LLM is a language model consisting of a neural network with many parameters (typically billions of weights or more), trained on large quantities of unlabeled text using self-supervised learning or semi-supervised learning.
5
7
  #
6
8
  # Langchain.rb provides a common interface to interact with all supported LLMs:
@@ -9,6 +11,7 @@ module Langchain::LLM
9
11
  # - {Langchain::LLM::Cohere}
10
12
  # - {Langchain::LLM::GooglePalm}
11
13
  # - {Langchain::LLM::HuggingFace}
14
+ # - {Langchain::LLM::LlamaCpp}
12
15
  # - {Langchain::LLM::OpenAI}
13
16
  # - {Langchain::LLM::Replicate}
14
17
  #
@@ -5,7 +5,7 @@ module Langchain::LLM
5
5
  # Wrapper around the Cohere API.
6
6
  #
7
7
  # Gem requirements:
8
- # gem "cohere-ruby", "~> 0.9.4"
8
+ # gem "cohere-ruby", "~> 0.9.5"
9
9
  #
10
10
  # Usage:
11
11
  # cohere = Langchain::LLM::Cohere.new(api_key: "YOUR_API_KEY")
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langchain::LLM
4
+ # A wrapper around the LlamaCpp.rb library
5
+ #
6
+ # Gem requirements:
7
+ # gem "llama_cpp"
8
+ #
9
+ # Usage:
10
+ # llama = Langchain::LLM::LlamaCpp.new(
11
+ # model_path: ENV["LLAMACPP_MODEL_PATH"],
12
+ # n_gpu_layers: Integer(ENV["LLAMACPP_N_GPU_LAYERS"]),
13
+ # n_threads: Integer(ENV["LLAMACPP_N_THREADS"])
14
+ # )
15
+ #
16
+ class LlamaCpp < Base
17
+ attr_accessor :model_path, :n_gpu_layers, :n_ctx, :seed
18
+ attr_writer :n_threads
19
+
20
+ # @param model_path [String] The path to the model to use
21
+ # @param n_gpu_layers [Integer] The number of GPU layers to use
22
+ # @param n_ctx [Integer] The number of context tokens to use
23
+ # @param n_threads [Integer] The CPU number of threads to use
24
+ # @param seed [Integer] The seed to use
25
+ def initialize(model_path:, n_gpu_layers: 1, n_ctx: 2048, n_threads: 1, seed: -1)
26
+ depends_on "llama_cpp"
27
+ require "llama_cpp"
28
+
29
+ @model_path = model_path
30
+ @n_gpu_layers = n_gpu_layers
31
+ @n_ctx = n_ctx
32
+ @n_threads = n_threads
33
+ @seed = seed
34
+ end
35
+
36
+ # @params text [String] The text to embed
37
+ # @params n_threads [Integer] The number of CPU threads to use
38
+ # @return [Array] The embedding
39
+ def embed(text:, n_threads: nil)
40
+ # contexts are kinda stateful when it comes to embeddings, so allocate one each time
41
+ context = embedding_context
42
+
43
+ embedding_input = context.tokenize(text: text, add_bos: true)
44
+ return unless embedding_input.size.positive?
45
+
46
+ n_threads ||= self.n_threads
47
+
48
+ context.eval(tokens: embedding_input, n_past: 0, n_threads: n_threads)
49
+ context.embeddings
50
+ end
51
+
52
+ # @params prompt [String] The prompt to complete
53
+ # @params n_predict [Integer] The number of tokens to predict
54
+ # @params n_threads [Integer] The number of CPU threads to use
55
+ # @return [String] The completed prompt
56
+ def complete(prompt:, n_predict: 128, n_threads: nil)
57
+ n_threads ||= self.n_threads
58
+ # contexts do not appear to be stateful when it comes to completion, so re-use the same one
59
+ context = completion_context
60
+ ::LLaMACpp.generate(context, prompt, n_threads: n_threads, n_predict: n_predict)
61
+ end
62
+
63
+ private
64
+
65
+ def n_threads
66
+ # Use the maximum number of CPU threads available, if not configured
67
+ @n_threads ||= `sysctl -n hw.ncpu`.strip.to_i
68
+ end
69
+
70
+ def build_context_params(embeddings: false)
71
+ context_params = ::LLaMACpp::ContextParams.new
72
+
73
+ context_params.seed = seed
74
+ context_params.n_ctx = n_ctx
75
+ context_params.n_gpu_layers = n_gpu_layers
76
+ context_params.embedding = embeddings
77
+
78
+ context_params
79
+ end
80
+
81
+ def build_model(embeddings: false)
82
+ return @model if defined?(@model)
83
+ @model = ::LLaMACpp::Model.new(model_path: model_path, params: build_context_params(embeddings: embeddings))
84
+ end
85
+
86
+ def build_completion_context
87
+ ::LLaMACpp::Context.new(model: build_model)
88
+ end
89
+
90
+ def build_embedding_context
91
+ ::LLaMACpp::Context.new(model: build_model(embeddings: true))
92
+ end
93
+
94
+ def completion_context
95
+ @completion_context ||= build_completion_context
96
+ end
97
+
98
+ def embedding_context
99
+ @embedding_context ||= build_embedding_context
100
+ end
101
+ end
102
+ end
@@ -125,7 +125,7 @@ module Langchain::LLM
125
125
 
126
126
  response = client.chat(parameters: parameters)
127
127
 
128
- raise "Chat completion failed: #{response}" if !response.empty? && response.dig("error")
128
+ raise Langchain::LLM::ApiError.new "Chat completion failed: #{response.dig("error", "message")}" if !response.empty? && response.dig("error")
129
129
 
130
130
  unless streaming
131
131
  response.dig("choices", 0, "message", "content")
@@ -51,6 +51,13 @@ module Langchain
51
51
  !!(@path =~ URI_REGEX)
52
52
  end
53
53
 
54
+ # Is the path a directory
55
+ #
56
+ # @return [Boolean] true if path is a directory
57
+ def directory?
58
+ File.directory?(@path)
59
+ end
60
+
54
61
  # Load data from a file or URL
55
62
  #
56
63
  # loader = Langchain::Loader.new("README.md")
@@ -69,15 +76,10 @@ module Langchain
69
76
  #
70
77
  # @return [Data] data that was loaded
71
78
  def load(&block)
72
- @raw_data = url? ? load_from_url : load_from_path
79
+ return process_data(load_from_url, &block) if url?
80
+ return load_from_directory(&block) if directory?
73
81
 
74
- data = if block
75
- yield @raw_data.read, @options
76
- else
77
- processor_klass.new(@options).parse(@raw_data)
78
- end
79
-
80
- Langchain::Data.new(data, source: @path)
82
+ process_data(load_from_path, &block)
81
83
  end
82
84
 
83
85
  private
@@ -92,6 +94,27 @@ module Langchain
92
94
  File.open(@path)
93
95
  end
94
96
 
97
+ def load_from_directory(&block)
98
+ Dir.glob(File.join(@path, "**/*")).map do |file|
99
+ # Only load and add to result files with supported extensions
100
+ Langchain::Loader.new(file, @options).load(&block)
101
+ rescue
102
+ UnknownFormatError nil
103
+ end.flatten.compact
104
+ end
105
+
106
+ def process_data(data, &block)
107
+ @raw_data = data
108
+
109
+ result = if block
110
+ yield @raw_data.read, @options
111
+ else
112
+ processor_klass.new(@options).parse(@raw_data)
113
+ end
114
+
115
+ Langchain::Data.new(result)
116
+ end
117
+
95
118
  def processor_klass
96
119
  raise UnknownFormatError unless (kind = find_processor)
97
120
 
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langchain::OutputParsers
4
+ # = Output Fixing Parser
5
+ #
6
+ class OutputFixingParser < Base
7
+ attr_reader :llm, :parser, :prompt
8
+
9
+ #
10
+ # Initializes a new instance of the class.
11
+ #
12
+ # @param llm [Langchain::LLM] The LLM used in the fixing process
13
+ # @param parser [Langchain::OutputParsers] The parser originally used which resulted in parsing error
14
+ # @param prompt [Langchain::Prompt::PromptTemplate]
15
+ #
16
+ def initialize(llm:, parser:, prompt:)
17
+ raise ArgumentError.new("llm must be an instance of Langchain::LLM got: #{llm.class}") unless llm.is_a?(Langchain::LLM::Base)
18
+ raise ArgumentError.new("parser must be an instance of Langchain::OutputParsers got #{parser.class}") unless parser.is_a?(Langchain::OutputParsers::Base)
19
+ raise ArgumentError.new("prompt must be an instance of Langchain::Prompt::PromptTemplate got #{prompt.class}") unless prompt.is_a?(Langchain::Prompt::PromptTemplate)
20
+ @llm = llm
21
+ @parser = parser
22
+ @prompt = prompt
23
+ end
24
+
25
+ def to_h
26
+ {
27
+ _type: "OutputFixingParser",
28
+ parser: parser.to_h,
29
+ prompt: prompt.to_h
30
+ }
31
+ end
32
+
33
+ #
34
+ # calls get_format_instructions on the @parser
35
+ #
36
+ # @return [String] Instructions for how the output of a language model should be formatted
37
+ # according to the @schema.
38
+ #
39
+ def get_format_instructions
40
+ parser.get_format_instructions
41
+ end
42
+
43
+ #
44
+ # Parse the output of an LLM call, if fails with OutputParserException
45
+ # then call the LLM with a fix prompt in an attempt to get the correctly
46
+ # formatted response
47
+ #
48
+ # @param completion [String] Text output from the LLM call
49
+ #
50
+ # @return [Object] object that is succesfully parsed by @parser.parse
51
+ #
52
+ def parse(completion)
53
+ parser.parse(completion)
54
+ rescue OutputParserException => e
55
+ new_completion = llm.chat(
56
+ prompt: prompt.format(
57
+ instructions: parser.get_format_instructions,
58
+ completion: completion,
59
+ error: e
60
+ )
61
+ )
62
+ parser.parse(new_completion)
63
+ end
64
+
65
+ #
66
+ # Creates a new instance of the class using the given JSON::Schema.
67
+ #
68
+ # @param schema [JSON::Schema] The JSON::Schema to use
69
+ #
70
+ # @return [Object] A new instance of the class
71
+ #
72
+ def self.from_llm(llm:, parser:, prompt: nil)
73
+ new(llm: llm, parser: parser, prompt: prompt || naive_fix_prompt)
74
+ end
75
+
76
+ private
77
+
78
+ private_class_method def self.naive_fix_prompt
79
+ Langchain::Prompt.load_from_path(
80
+ file_path: Langchain.root.join("langchain/output_parsers/prompts/naive_fix_prompt.yaml")
81
+ )
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,22 @@
1
+ _type: prompt
2
+ input_variables:
3
+ - instructions
4
+ - completion
5
+ - error
6
+ template: |
7
+ Instructions:
8
+ --------------
9
+ {instructions}
10
+ --------------
11
+ Completion:
12
+ --------------
13
+ {completion}
14
+ --------------
15
+
16
+ Above, the Completion did not satisfy the constraints given in the Instructions.
17
+ Error:
18
+ --------------
19
+ {error}
20
+ --------------
21
+
22
+ Please try again. Please only respond with an answer that satisfies the constraints laid out in the Instructions:
@@ -37,6 +37,9 @@ module Langchain
37
37
  #
38
38
  def self.token_length(text, model_name = "chat-bison-001", options)
39
39
  response = options[:llm].client.count_message_tokens(model: model_name, prompt: text)
40
+
41
+ raise Langchain::LLM::ApiError.new(response["error"]["message"]) unless response["error"].nil?
42
+
40
43
  response.dig("tokenCount")
41
44
  end
42
45
 
@@ -98,6 +98,11 @@ module Langchain::Vectorsearch
98
98
  @llm = llm
99
99
  end
100
100
 
101
+ # Method supported by Vectorsearch DB to retrieve a default schema
102
+ def get_default_schema
103
+ raise NotImplementedError, "#{self.class.name} does not support retrieving a default schema"
104
+ end
105
+
101
106
  # Method supported by Vectorsearch DB to create a default schema
102
107
  def create_default_schema
103
108
  raise NotImplementedError, "#{self.class.name} does not support creating a default schema"
@@ -67,10 +67,17 @@ module Langchain::Vectorsearch
67
67
  ::Chroma::Resources::Collection.create(index_name)
68
68
  end
69
69
 
70
- # TODO: Uncomment and add the spec
71
- # def destroy_default_schema
72
- # ::Chroma::Resources::Collection.delete(index_name)
73
- # end
70
+ # Get the default schema
71
+ # @return [Hash] The response from the server
72
+ def get_default_schema
73
+ ::Chroma::Resources::Collection.get(index_name)
74
+ end
75
+
76
+ # Delete the default schema
77
+ # @return [Hash] The response from the server
78
+ def destroy_default_schema
79
+ ::Chroma::Resources::Collection.delete(index_name)
80
+ end
74
81
 
75
82
  # Search for similar texts
76
83
  # @param query [String] The text to search for
@@ -10,8 +10,7 @@ module Langchain::Vectorsearch
10
10
  # gem "hnswlib", "~> 0.8.1"
11
11
  #
12
12
  # Usage:
13
- # hnsw = Langchain::Vectorsearch::Hnswlib.new(llm:, url:, index_name:)
14
- #
13
+ # hnsw = Langchain::Vectorsearch::Hnswlib.new(llm:, path_to_index:)
15
14
 
16
15
  attr_reader :client, :path_to_index
17
16
 
@@ -79,7 +79,17 @@ module Langchain::Vectorsearch
79
79
  )
80
80
  end
81
81
 
82
- # TODO: Add destroy_default_schema method
82
+ # Get the default schema
83
+ # @return [Hash] The response from the server
84
+ def get_default_schema
85
+ client.collections.get(collection_name: index_name)
86
+ end
87
+
88
+ # Delete default schema
89
+ # @return [Hash] The response from the server
90
+ def destroy_default_schema
91
+ client.collections.delete(collection_name: index_name)
92
+ end
83
93
 
84
94
  def similarity_search(query:, k: 4)
85
95
  embedding = llm.embed(text: query)
@@ -85,6 +85,12 @@ module Langchain::Vectorsearch
85
85
  client.delete_index(index_name)
86
86
  end
87
87
 
88
+ # Get the default schema
89
+ # @return [Pinecone::Vector] The default schema
90
+ def get_default_schema
91
+ index
92
+ end
93
+
88
94
  # Search for similar texts
89
95
  # @param query [String] The text to search for
90
96
  # @param k [Integer] The number of results to return
@@ -32,11 +32,12 @@ module Langchain::Vectorsearch
32
32
  # Add a list of texts to the index
33
33
  # @param texts [Array] The list of texts to add
34
34
  # @return [Hash] The response from the server
35
- def add_texts(texts:, ids:)
35
+ def add_texts(texts:, ids: [])
36
36
  batch = {ids: [], vectors: [], payloads: []}
37
37
 
38
38
  Array(texts).each_with_index do |text, i|
39
- batch[:ids].push(ids[i] || SecureRandom.uuid)
39
+ id = ids[i] || SecureRandom.uuid
40
+ batch[:ids].push(id)
40
41
  batch[:vectors].push(llm.embed(text: text))
41
42
  batch[:payloads].push({content: text})
42
43
  end
@@ -51,6 +52,12 @@ module Langchain::Vectorsearch
51
52
  add_texts(texts: texts, ids: ids)
52
53
  end
53
54
 
55
+ # Get the default schema
56
+ # @return [Hash] The response from the server
57
+ def get_default_schema
58
+ client.collections.get(collection_name: index_name)
59
+ end
60
+
54
61
  # Deletes the default schema
55
62
  # @return [Hash] The response from the server
56
63
  def destroy_default_schema
@@ -109,7 +116,7 @@ module Langchain::Vectorsearch
109
116
  def ask(question:)
110
117
  search_results = similarity_search(query: question)
111
118
 
112
- context = search_results.dig("result").map do |result|
119
+ context = search_results.map do |result|
113
120
  result.dig("payload").to_s
114
121
  end
115
122
  context = context.join("\n---\n")
@@ -85,6 +85,12 @@ module Langchain::Vectorsearch
85
85
  )
86
86
  end
87
87
 
88
+ # Get default schema
89
+ # @return [Hash] The response from the server
90
+ def get_default_schema
91
+ client.schema.get(class_name: index_name)
92
+ end
93
+
88
94
  # Delete the index
89
95
  # @return [Boolean] Whether the index was deleted
90
96
  def destroy_default_schema
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Langchain
4
- VERSION = "0.6.3"
4
+ VERSION = "0.6.5"
5
5
  end
data/lib/langchain.rb CHANGED
@@ -51,6 +51,7 @@ module Langchain
51
51
  autoload :Loader, "langchain/loader"
52
52
  autoload :Data, "langchain/data"
53
53
  autoload :Conversation, "langchain/conversation"
54
+ autoload :ConversationMemory, "langchain/conversation_memory"
54
55
  autoload :DependencyHelper, "langchain/dependency_helper"
55
56
  autoload :ContextualLogger, "langchain/contextual_logger"
56
57
 
@@ -133,6 +134,7 @@ module Langchain
133
134
  autoload :Cohere, "langchain/llm/cohere"
134
135
  autoload :GooglePalm, "langchain/llm/google_palm"
135
136
  autoload :HuggingFace, "langchain/llm/hugging_face"
137
+ autoload :LlamaCpp, "langchain/llm/llama_cpp"
136
138
  autoload :OpenAI, "langchain/llm/openai"
137
139
  autoload :Replicate, "langchain/llm/replicate"
138
140
  end
@@ -152,6 +154,7 @@ module Langchain
152
154
  module OutputParsers
153
155
  autoload :Base, "langchain/output_parsers/base"
154
156
  autoload :StructuredOutputParser, "langchain/output_parsers/structured"
157
+ autoload :OutputFixingParser, "langchain/output_parsers/fix"
155
158
  end
156
159
 
157
160
  module Errors
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: langchainrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.3
4
+ version: 0.6.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Bondarev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-06-26 00:00:00.000000000 Z
11
+ date: 2023-07-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: baran
@@ -262,6 +262,20 @@ dependencies:
262
262
  - - "~>"
263
263
  - !ruby/object:Gem::Version
264
264
  version: 0.9.0
265
+ - !ruby/object:Gem::Dependency
266
+ name: llama_cpp
267
+ requirement: !ruby/object:Gem::Requirement
268
+ requirements:
269
+ - - ">="
270
+ - !ruby/object:Gem::Version
271
+ version: '0'
272
+ type: :development
273
+ prerelease: false
274
+ version_requirements: !ruby/object:Gem::Requirement
275
+ requirements:
276
+ - - ">="
277
+ - !ruby/object:Gem::Version
278
+ version: '0'
265
279
  - !ruby/object:Gem::Dependency
266
280
  name: nokogiri
267
281
  requirement: !ruby/object:Gem::Requirement
@@ -474,9 +488,11 @@ files:
474
488
  - LICENSE.txt
475
489
  - README.md
476
490
  - Rakefile
491
+ - examples/conversation_with_openai.rb
477
492
  - examples/create_and_manage_few_shot_prompt_templates.rb
478
493
  - examples/create_and_manage_prompt_templates.rb
479
494
  - examples/create_and_manage_prompt_templates_using_structured_output_parser.rb
495
+ - examples/llama_cpp.rb
480
496
  - examples/pdf_store_and_query_with_chroma.rb
481
497
  - examples/store_and_query_with_pinecone.rb
482
498
  - examples/store_and_query_with_qdrant.rb
@@ -494,6 +510,7 @@ files:
494
510
  - lib/langchain/chunker/text.rb
495
511
  - lib/langchain/contextual_logger.rb
496
512
  - lib/langchain/conversation.rb
513
+ - lib/langchain/conversation_memory.rb
497
514
  - lib/langchain/data.rb
498
515
  - lib/langchain/dependency_helper.rb
499
516
  - lib/langchain/llm/ai21.rb
@@ -501,11 +518,14 @@ files:
501
518
  - lib/langchain/llm/cohere.rb
502
519
  - lib/langchain/llm/google_palm.rb
503
520
  - lib/langchain/llm/hugging_face.rb
521
+ - lib/langchain/llm/llama_cpp.rb
504
522
  - lib/langchain/llm/openai.rb
505
523
  - lib/langchain/llm/prompts/summarize_template.yaml
506
524
  - lib/langchain/llm/replicate.rb
507
525
  - lib/langchain/loader.rb
508
526
  - lib/langchain/output_parsers/base.rb
527
+ - lib/langchain/output_parsers/fix.rb
528
+ - lib/langchain/output_parsers/prompts/naive_fix_prompt.yaml
509
529
  - lib/langchain/output_parsers/structured.rb
510
530
  - lib/langchain/processors/base.rb
511
531
  - lib/langchain/processors/csv.rb