langchainrb 0.16.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f685910b0f3f1816c3822debc2ec470d72d85203b2695ab8ab780b5d0f1cb09
4
- data.tar.gz: 7ec406ad7980e12739aa70e9710b21e1f7df0a1e46f66820d7003026e3bbc877
3
+ metadata.gz: b078089a99e9e8d6654a244165ecc9d0f3dfdd8fbc0367623d41fe771a98ac41
4
+ data.tar.gz: 890c371564ce9188087bed9eb053a59e11f7b734a44b9f753696f8458f8a7b7e
5
5
  SHA512:
6
- metadata.gz: 1145ffbab814f09acb539df3662f0c4c5536ded25252a4db3e640d29cc550930b11ac9310b11436d591e1ec13449af59f125bdab3fe463f9a59683ea9d8f38ed
7
- data.tar.gz: d9e76d70f24f964b3f17addda08ace46695149af48c7bd4aff1936a10888640f9cb222bef15bdb205ad7997028f88c81142dcb3ef49d96fcd37682c56409f4c6
6
+ metadata.gz: 8f458bfae5af31190f41661a13c24e5cd63d5f88e594e854ee79ea3b8af1f51b20552f178c89c71e454a86e4d827b3facecd97f0fa3ef107b7b6097754fab5e3
7
+ data.tar.gz: cfe0c684f89c5eef73ceb26b70292fe8fc4f941e13795ac98bd1d3197321a1303250d2584f9e45aa530e311304004911ffe3a6af7f606f6a733baad21ff2b814
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.16.1] - 2024-09-30
4
+ - Deprecate Langchain::LLM::GooglePalm
5
+ - Allow setting response_object: {} parameter when initializing supported Langchain::LLM::* classes
6
+ - Simplify and consolidate logging for some of the LLM providers (namely OpenAI and Google). Now most of the HTTP requests are being logged when on DEBUG level
7
+ - Improve doc on how to set up a custom logger with a custom destination
8
+
3
9
  ## [0.16.0] - 2024-09-19
4
10
  - Remove `Langchain::Thread` class as it was not needed.
5
11
  - Support `cohere` provider for `Langchain::LLM::AwsBedrock#embed`
data/README.md CHANGED
@@ -21,7 +21,7 @@ Available for paid consulting engagements! [Email me](mailto:andrei@sourcelabs.i
21
21
 
22
22
  - [Installation](#installation)
23
23
  - [Usage](#usage)
24
- - [Large Language Models (LLMs)](#large-language-models-llms)
24
+ - [Unified Interface for LLMs](#unified-interface-for-llms)
25
25
  - [Prompt Management](#prompt-management)
26
26
  - [Output Parsers](#output-parsers)
27
27
  - [Building RAG](#building-retrieval-augment-generation-rag-system)
@@ -51,61 +51,139 @@ Additional gems may be required. They're not included by default so you can incl
51
51
  require "langchain"
52
52
  ```
53
53
 
54
- ## Large Language Models (LLMs)
55
- Langchain.rb wraps supported LLMs in a unified interface allowing you to easily swap out and test out different models.
54
+ # Unified Interface for LLMs
56
55
 
57
- #### Supported LLMs and features:
58
- | LLM providers | `embed()` | `complete()` | `chat()` | `summarize()` | Notes |
59
- | -------- |:------------------:| :-------: | :-----------------: | :-------: | :----------------- |
60
- | [OpenAI](https://openai.com/?utm_source=langchainrb&utm_medium=github) | ✅ | ✅ | ✅ | ✅ | Including Azure OpenAI |
61
- | [AI21](https://ai21.com/?utm_source=langchainrb&utm_medium=github) | ❌ | ✅ | ❌ | ✅ | |
62
- | [Anthropic](https://anthropic.com/?utm_source=langchainrb&utm_medium=github) | ❌ | ✅ | ✅ | ❌ | |
63
- | [AwsBedrock](https://aws.amazon.com/bedrock?utm_source=langchainrb&utm_medium=github) | ✅ | ✅ | ✅ | ❌ | Provides AWS, Cohere, AI21, Antropic and Stability AI models |
64
- | [Cohere](https://cohere.com/?utm_source=langchainrb&utm_medium=github) | ✅ | ✅ | ✅ | ✅ | |
65
- | [GooglePalm](https://ai.google/discover/palm2?utm_source=langchainrb&utm_medium=github) | ✅ | ✅ | ✅ | ✅ | |
66
- | [GoogleVertexAI](https://cloud.google.com/vertex-ai?utm_source=langchainrb&utm_medium=github) | ✅ | ❌ | ✅ | ❌ | Requires Google Cloud service auth |
67
- | [GoogleGemini](https://cloud.google.com/vertex-ai?utm_source=langchainrb&utm_medium=github) | ✅ | ❌ | ✅ | ❌ | Requires Gemini API Key ([get key](https://ai.google.dev/gemini-api/docs/api-key)) |
68
- | [HuggingFace](https://huggingface.co/?utm_source=langchainrb&utm_medium=github) | ✅ | ❌ | ❌ | ❌ | |
69
- | [MistralAI](https://mistral.ai/?utm_source=langchainrb&utm_medium=github) | ✅ | ❌ | ✅ | ❌ | |
70
- | [Ollama](https://ollama.ai/?utm_source=langchainrb&utm_medium=github) | ✅ | ✅ | ✅ | ✅ | |
71
- | [Replicate](https://replicate.com/?utm_source=langchainrb&utm_medium=github) | ✅ | ✅ | ✅ | ✅ | |
56
+ The `Langchain::LLM` module provides a unified interface for interacting with various Large Language Model (LLM) providers. This abstraction allows you to easily switch between different LLM backends without changing your application code.
72
57
 
58
+ ## Supported LLM Providers
73
59
 
60
+ - AI21
61
+ - Anthropic
62
+ - AWS Bedrock
63
+ - Azure OpenAI
64
+ - Cohere
65
+ - Google Gemini
66
+ - Google PaLM (deprecated)
67
+ - Google Vertex AI
68
+ - HuggingFace
69
+ - LlamaCpp
70
+ - Mistral AI
71
+ - Ollama
72
+ - OpenAI
73
+ - Replicate
74
74
 
75
- #### Using standalone LLMs:
75
+ ## Usage
76
76
 
77
- #### OpenAI
77
+ All LLM classes inherit from `Langchain::LLM::Base` and provide a consistent interface for common operations:
78
78
 
79
- Add `gem "ruby-openai", "~> 6.3.0"` to your Gemfile.
79
+ 1. Generating embeddings
80
+ 2. Generating prompt completions
81
+ 3. Generating chat completions
82
+
83
+ ### Initialization
84
+
85
+ Most LLM classes can be initialized with an API key and optional default options:
80
86
 
81
87
  ```ruby
82
- llm = Langchain::LLM::OpenAI.new(api_key: ENV["OPENAI_API_KEY"])
83
- ```
84
- You can pass additional parameters to the constructor, it will be passed to the OpenAI client:
85
- ```ruby
86
- llm = Langchain::LLM::OpenAI.new(api_key: ENV["OPENAI_API_KEY"], llm_options: { ... })
88
+ llm = Langchain::LLM::OpenAI.new(
89
+ api_key: ENV["OPENAI_API_KEY"],
90
+ default_options: { temperature: 0.7, chat_completion_model_name: "gpt-4o" }
91
+ )
87
92
  ```
88
93
 
89
- Generate vector embeddings:
94
+ ### Generating Embeddings
95
+
96
+ Use the `embed` method to generate embeddings for given text:
97
+
90
98
  ```ruby
91
- llm.embed(text: "foo bar").embedding
99
+ response = llm.embed(text: "Hello, world!")
100
+ embedding = response.embedding
92
101
  ```
93
102
 
94
- Generate a chat completion:
103
+ #### Accepted parameters for `embed()`
104
+
105
+ - `text`: (Required) The input text to embed.
106
+ - `model`: (Optional) The model name to use or default embedding model will be used.
107
+
108
+ ### Prompt completions
109
+
110
+ Use the `complete` method to generate completions for a given prompt:
111
+
95
112
  ```ruby
96
- llm.chat(messages: [{role: "user", content: "What is the meaning of life?"}]).completion
113
+ response = llm.complete(prompt: "Once upon a time")
114
+ completion = response.completion
97
115
  ```
98
116
 
99
- Summarize the text:
117
+ #### Accepted parameters for `complete()`
118
+
119
+ - `prompt`: (Required) The input prompt for completion.
120
+ - `max_tokens`: (Optional) The maximum number of tokens to generate.
121
+ - `temperature`: (Optional) Controls randomness in generation. Higher values (e.g., 0.8) make output more random, while lower values (e.g., 0.2) make it more deterministic.
122
+ - `top_p`: (Optional) An alternative to temperature, controls diversity of generated tokens.
123
+ - `n`: (Optional) Number of completions to generate for each prompt.
124
+ - `stop`: (Optional) Sequences where the API will stop generating further tokens.
125
+ - `presence_penalty`: (Optional) Penalizes new tokens based on their presence in the text so far.
126
+ - `frequency_penalty`: (Optional) Penalizes new tokens based on their frequency in the text so far.
127
+
128
+ ### Generating Chat Completions
129
+
130
+ Use the `chat` method to generate chat completions:
131
+
100
132
  ```ruby
101
- llm.summarize(text: "...").completion
133
+ messages = [
134
+ { role: "system", content: "You are a helpful assistant." },
135
+ { role: "user", content: "What's the weather like today?" }
136
+ ]
137
+ response = llm.chat(messages: messages)
138
+ chat_completion = response.chat_completion
102
139
  ```
103
140
 
104
- You can use any other LLM by invoking the same interface:
141
+ #### Accepted parameters for `chat()`
142
+
143
+ - `messages`: (Required) An array of message objects representing the conversation history.
144
+ - `model`: (Optional) The specific chat model to use.
145
+ - `temperature`: (Optional) Controls randomness in generation.
146
+ - `top_p`: (Optional) An alternative to temperature, controls diversity of generated tokens.
147
+ - `n`: (Optional) Number of chat completion choices to generate.
148
+ - `max_tokens`: (Optional) The maximum number of tokens to generate in the chat completion.
149
+ - `stop`: (Optional) Sequences where the API will stop generating further tokens.
150
+ - `presence_penalty`: (Optional) Penalizes new tokens based on their presence in the text so far.
151
+ - `frequency_penalty`: (Optional) Penalizes new tokens based on their frequency in the text so far.
152
+ - `logit_bias`: (Optional) Modifies the likelihood of specified tokens appearing in the completion.
153
+ - `user`: (Optional) A unique identifier representing your end-user.
154
+ - `tools`: (Optional) A list of tools the model may call.
155
+ - `tool_choice`: (Optional) Controls how the model calls functions.
156
+
157
+ ## Switching LLM Providers
158
+
159
+ Thanks to the unified interface, you can easily switch between different LLM providers by changing the class you instantiate:
160
+
105
161
  ```ruby
106
- llm = Langchain::LLM::GooglePalm.new(api_key: ENV["GOOGLE_PALM_API_KEY"], default_options: { ... })
162
+ # Using Anthropic
163
+ anthropic_llm = Langchain::LLM::Anthropic.new(api_key: ENV["ANTHROPIC_API_KEY"])
164
+
165
+ # Using Google Gemini
166
+ gemini_llm = Langchain::LLM::GoogleGemini.new(api_key: ENV["GOOGLE_GEMINI_API_KEY"])
167
+
168
+ # Using OpenAI
169
+ openai_llm = Langchain::LLM::OpenAI.new(api_key: ENV["OPENAI_API_KEY"])
107
170
  ```
108
171
 
172
+ ## Response Objects
173
+
174
+ Each LLM method returns a response object that provides a consistent interface for accessing the results:
175
+
176
+ - `embedding`: Returns the embedding vector
177
+ - `completion`: Returns the generated text completion
178
+ - `chat_completion`: Returns the generated chat completion
179
+ - `tool_calls`: Returns tool calls made by the LLM
180
+ - `prompt_tokens`: Returns the number of tokens in the prompt
181
+ - `completion_tokens`: Returns the number of tokens in the completion
182
+ - `total_tokens`: Returns the total number of tokens used
183
+
184
+ > [!NOTE]
185
+ > While the core interface is consistent across providers, some LLMs may offer additional features or parameters. Consult the documentation for each LLM class to learn about provider-specific capabilities and options.
186
+
109
187
  ### Prompt Management
110
188
 
111
189
  #### Prompt Templates
@@ -427,7 +505,19 @@ assistant.add_message_and_run!(content: "What's the latest news about AI?")
427
505
  messages = assistant.messages
428
506
 
429
507
  # Run the assistant with automatic tool execution
430
- assistant.run!
508
+ assistant.run(auto_tool_execution: true)
509
+
510
+ # If you want to stream the response, you can add a response handler
511
+ assistant = Langchain::Assistant.new(
512
+ llm: llm,
513
+ instructions: "You're a helpful AI assistant",
514
+ tools: [Langchain::Tool::NewsRetriever.new(api_key: ENV["NEWS_API_KEY"])]
515
+ ) do |response_chunk|
516
+ # ...handle the response stream
517
+ # print(response_chunk.inspect)
518
+ end
519
+ assistant.add_message(content: "Hello")
520
+ assistant.run(auto_tool_execution: true)
431
521
  ```
432
522
 
433
523
  ### Configuration
@@ -536,11 +626,18 @@ Additional examples available: [/examples](https://github.com/andreibondarev/lan
536
626
 
537
627
  ## Logging
538
628
 
539
- Langchain.rb uses standard logging mechanisms and defaults to `:warn` level. Most messages are at info level, but we will add debug or warn statements as needed.
629
+ Langchain.rb uses the standard Ruby [Logger](https://ruby-doc.org/stdlib-2.4.0/libdoc/logger/rdoc/Logger.html) mechanism and defaults to same `level` value (currently `Logger::DEBUG`).
630
+
540
631
  To show all log messages:
541
632
 
542
633
  ```ruby
543
- Langchain.logger.level = :debug
634
+ Langchain.logger.level = Logger::DEBUG
635
+ ```
636
+
637
+ The logger logs to `STDOUT` by default. In order to configure the log destination (ie. log to a file) do:
638
+
639
+ ```ruby
640
+ Langchain.logger = Logger.new("path/to/file", **Langchain::LOGGER_OPTIONS)
544
641
  ```
545
642
 
546
643
  ## Problems
@@ -29,7 +29,8 @@ module Langchain
29
29
  instructions: nil,
30
30
  tool_choice: "auto",
31
31
  messages: [],
32
- add_message_callback: nil
32
+ add_message_callback: nil,
33
+ &block
33
34
  )
34
35
  unless tools.is_a?(Array) && tools.all? { |tool| tool.class.singleton_class.included_modules.include?(Langchain::ToolDefinition) }
35
36
  raise ArgumentError, "Tools must be an array of objects extending Langchain::ToolDefinition"
@@ -48,6 +49,7 @@ module Langchain
48
49
  @tools = tools
49
50
  self.tool_choice = tool_choice
50
51
  @instructions = instructions
52
+ @block = block
51
53
  @state = :ready
52
54
 
53
55
  @total_prompt_tokens = 0
@@ -120,7 +122,7 @@ module Langchain
120
122
  # @return [Array<Langchain::Message>] The messages
121
123
  def run(auto_tool_execution: false)
122
124
  if messages.empty?
123
- Langchain.logger.warn("No messages to process")
125
+ Langchain.logger.warn("#{self.class} - No messages to process")
124
126
  @state = :completed
125
127
  return
126
128
  end
@@ -270,7 +272,7 @@ module Langchain
270
272
  #
271
273
  # @return [Symbol] The completed state
272
274
  def handle_system_message
273
- Langchain.logger.warn("At least one user message is required after a system message")
275
+ Langchain.logger.warn("#{self.class} - At least one user message is required after a system message")
274
276
  :completed
275
277
  end
276
278
 
@@ -285,7 +287,7 @@ module Langchain
285
287
  #
286
288
  # @return [Symbol] The failed state
287
289
  def handle_unexpected_message
288
- Langchain.logger.error("Unexpected message role encountered: #{messages.last.standard_role}")
290
+ Langchain.logger.error("#{self.class} - Unexpected message role encountered: #{messages.last.standard_role}")
289
291
  :failed
290
292
  end
291
293
 
@@ -309,7 +311,7 @@ module Langchain
309
311
  elsif response.completion # Currently only used by Ollama
310
312
  :completed
311
313
  else
312
- Langchain.logger.error("LLM response does not contain tool calls, chat or completion response")
314
+ Langchain.logger.error("#{self.class} - LLM response does not contain tool calls, chat or completion response")
313
315
  :failed
314
316
  end
315
317
  end
@@ -321,7 +323,7 @@ module Langchain
321
323
  run_tools(messages.last.tool_calls)
322
324
  :in_progress
323
325
  rescue => e
324
- Langchain.logger.error("Error running tools: #{e.message}; #{e.backtrace.join('\n')}")
326
+ Langchain.logger.error("#{self.class} - Error running tools: #{e.message}; #{e.backtrace.join('\n')}")
325
327
  :failed
326
328
  end
327
329
 
@@ -353,7 +355,7 @@ module Langchain
353
355
  #
354
356
  # @return [Langchain::LLM::BaseResponse] The LLM response object
355
357
  def chat_with_llm
356
- Langchain.logger.info("Sending a call to #{llm.class}", for: self.class)
358
+ Langchain.logger.debug("#{self.class} - Sending a call to #{llm.class}")
357
359
 
358
360
  params = @llm_adapter.build_chat_params(
359
361
  instructions: @instructions,
@@ -361,7 +363,7 @@ module Langchain
361
363
  tools: @tools,
362
364
  tool_choice: tool_choice
363
365
  )
364
- @llm.chat(**params)
366
+ @llm.chat(**params, &@block)
365
367
  end
366
368
 
367
369
  # Run the tools automatically
@@ -38,7 +38,8 @@ module Langchain::LLM
38
38
  top_logprobs: {},
39
39
  n: {default: @defaults[:n]},
40
40
  temperature: {default: @defaults[:temperature]},
41
- user: {}
41
+ user: {},
42
+ response_format: {default: @defaults[:response_format]}
42
43
  )
43
44
  chat_parameters.ignore(:top_k)
44
45
  end
@@ -24,7 +24,10 @@ module Langchain::LLM
24
24
  include Langchain::DependencyHelper
25
25
 
26
26
  # A client for communicating with the LLM
27
- attr_reader :client
27
+ attr_accessor :client
28
+
29
+ # Default LLM options. Can be overridden by passing `default_options: {}` to the Langchain::LLM::* constructors.
30
+ attr_reader :defaults
28
31
 
29
32
  # Ensuring backward compatibility after https://github.com/patterns-ai-core/langchainrb/pull/586
30
33
  # TODO: Delete this method later
@@ -27,7 +27,8 @@ module Langchain::LLM
27
27
  @defaults = DEFAULTS.merge(default_options)
28
28
  chat_parameters.update(
29
29
  model: {default: @defaults[:chat_completion_model_name]},
30
- temperature: {default: @defaults[:temperature]}
30
+ temperature: {default: @defaults[:temperature]},
31
+ response_format: {default: @defaults[:response_format]}
31
32
  )
32
33
  chat_parameters.remap(
33
34
  system: :preamble,
@@ -97,6 +98,10 @@ module Langchain::LLM
97
98
 
98
99
  parameters = chat_parameters.to_params(params)
99
100
 
101
+ # Cohere API requires `message:` parameter to be sent separately from `chat_history:`.
102
+ # We extract the last message from the messages param.
103
+ parameters[:message] = parameters[:chat_history].pop&.dig(:message)
104
+
100
105
  response = client.chat(**parameters)
101
106
 
102
107
  Langchain::LLM::CohereResponse.new(response)
@@ -59,15 +59,7 @@ module Langchain::LLM
59
59
 
60
60
  uri = URI("https://generativelanguage.googleapis.com/v1beta/models/#{parameters[:model]}:generateContent?key=#{api_key}")
61
61
 
62
- request = Net::HTTP::Post.new(uri)
63
- request.content_type = "application/json"
64
- request.body = parameters.to_json
65
-
66
- response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
67
- http.request(request)
68
- end
69
-
70
- parsed_response = JSON.parse(response.body)
62
+ parsed_response = http_post(uri, parameters)
71
63
 
72
64
  wrapped_response = Langchain::LLM::GoogleGeminiResponse.new(parsed_response, model: parameters[:model])
73
65
 
@@ -95,17 +87,25 @@ module Langchain::LLM
95
87
 
96
88
  uri = URI("https://generativelanguage.googleapis.com/v1beta/models/#{model}:embedContent?key=#{api_key}")
97
89
 
98
- request = Net::HTTP::Post.new(uri)
90
+ parsed_response = http_post(uri, params)
91
+
92
+ Langchain::LLM::GoogleGeminiResponse.new(parsed_response, model: model)
93
+ end
94
+
95
+ private
96
+
97
+ def http_post(url, params)
98
+ http = Net::HTTP.new(url.hostname, url.port)
99
+ http.use_ssl = url.scheme == "https"
100
+ http.set_debug_output(Langchain.logger) if Langchain.logger.debug?
101
+
102
+ request = Net::HTTP::Post.new(url)
99
103
  request.content_type = "application/json"
100
104
  request.body = params.to_json
101
105
 
102
- response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
103
- http.request(request)
104
- end
105
-
106
- parsed_response = JSON.parse(response.body)
106
+ response = http.request(request)
107
107
 
108
- Langchain::LLM::GoogleGeminiResponse.new(parsed_response, model: model)
108
+ JSON.parse(response.body)
109
109
  end
110
110
  end
111
111
  end
@@ -11,6 +11,8 @@ module Langchain::LLM
11
11
  # google_palm = Langchain::LLM::GooglePalm.new(api_key: ENV["GOOGLE_PALM_API_KEY"])
12
12
  #
13
13
  class GooglePalm < Base
14
+ extend Gem::Deprecate
15
+
14
16
  DEFAULTS = {
15
17
  temperature: 0.0,
16
18
  dimensions: 768, # This is what the `embedding-gecko-001` model generates
@@ -25,12 +27,16 @@ module Langchain::LLM
25
27
 
26
28
  attr_reader :defaults
27
29
 
30
+ # @deprecated Please use Langchain::LLM::GoogleGemini instead
31
+ #
32
+ # @param api_key [String] The API key for the Google PaLM API
28
33
  def initialize(api_key:, default_options: {})
29
34
  depends_on "google_palm_api"
30
35
 
31
36
  @client = ::GooglePalmApi::Client.new(api_key: api_key)
32
37
  @defaults = DEFAULTS.merge(default_options)
33
38
  end
39
+ deprecate :initialize, "Langchain::LLM::GoogleGemini.new(api_key:)", 2024, 10
34
40
 
35
41
  #
36
42
  # Generate an embedding for a given text
@@ -63,16 +63,7 @@ module Langchain::LLM
63
63
 
64
64
  uri = URI("#{url}#{model}:predict")
65
65
 
66
- request = Net::HTTP::Post.new(uri)
67
- request.content_type = "application/json"
68
- request["Authorization"] = "Bearer #{@authorizer.fetch_access_token!["access_token"]}"
69
- request.body = params.to_json
70
-
71
- response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
72
- http.request(request)
73
- end
74
-
75
- parsed_response = JSON.parse(response.body)
66
+ parsed_response = http_post(uri, params)
76
67
 
77
68
  Langchain::LLM::GoogleGeminiResponse.new(parsed_response, model: model)
78
69
  end
@@ -96,16 +87,7 @@ module Langchain::LLM
96
87
 
97
88
  uri = URI("#{url}#{parameters[:model]}:generateContent")
98
89
 
99
- request = Net::HTTP::Post.new(uri)
100
- request.content_type = "application/json"
101
- request["Authorization"] = "Bearer #{@authorizer.fetch_access_token!["access_token"]}"
102
- request.body = parameters.to_json
103
-
104
- response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
105
- http.request(request)
106
- end
107
-
108
- parsed_response = JSON.parse(response.body)
90
+ parsed_response = http_post(uri, parameters)
109
91
 
110
92
  wrapped_response = Langchain::LLM::GoogleGeminiResponse.new(parsed_response, model: parameters[:model])
111
93
 
@@ -115,5 +97,22 @@ module Langchain::LLM
115
97
  raise StandardError.new(parsed_response)
116
98
  end
117
99
  end
100
+
101
+ private
102
+
103
+ def http_post(url, params)
104
+ http = Net::HTTP.new(url.hostname, url.port)
105
+ http.use_ssl = url.scheme == "https"
106
+ http.set_debug_output(Langchain.logger) if Langchain.logger.debug?
107
+
108
+ request = Net::HTTP::Post.new(url)
109
+ request.content_type = "application/json"
110
+ request["Authorization"] = "Bearer #{@authorizer.fetch_access_token!["access_token"]}"
111
+ request.body = params.to_json
112
+
113
+ response = http.request(request)
114
+
115
+ JSON.parse(response.body)
116
+ end
118
117
  end
119
118
  end
@@ -26,7 +26,9 @@ module Langchain::LLM
26
26
  chat_parameters.update(
27
27
  model: {default: @defaults[:chat_completion_model_name]},
28
28
  n: {default: @defaults[:n]},
29
- safe_prompt: {}
29
+ safe_prompt: {},
30
+ temperature: {default: @defaults[:temperature]},
31
+ response_format: {default: @defaults[:response_format]}
30
32
  )
31
33
  chat_parameters.remap(seed: :random_seed)
32
34
  chat_parameters.ignore(:n, :top_k)
@@ -45,7 +45,8 @@ module Langchain::LLM
45
45
  model: {default: @defaults[:chat_completion_model_name]},
46
46
  temperature: {default: @defaults[:temperature]},
47
47
  template: {},
48
- stream: {default: false}
48
+ stream: {default: false},
49
+ response_format: {default: @defaults[:response_format]}
49
50
  )
50
51
  chat_parameters.remap(response_format: :format)
51
52
  end
@@ -149,7 +150,7 @@ module Langchain::LLM
149
150
  end
150
151
  end
151
152
 
152
- generate_final_completion_response(responses_stream, parameters)
153
+ generate_final_completion_response(responses_stream, parameters[:model])
153
154
  end
154
155
 
155
156
  # Generate a chat completion
@@ -186,7 +187,7 @@ module Langchain::LLM
186
187
  end
187
188
  end
188
189
 
189
- generate_final_chat_completion_response(responses_stream, parameters)
190
+ generate_final_chat_completion_response(responses_stream, parameters[:model])
190
191
  end
191
192
 
192
193
  #
@@ -289,20 +290,20 @@ module Langchain::LLM
289
290
  end
290
291
  end
291
292
 
292
- def generate_final_completion_response(responses_stream, parameters)
293
+ def generate_final_completion_response(responses_stream, model)
293
294
  final_response = responses_stream.last.merge(
294
295
  "response" => responses_stream.map { |resp| resp["response"] }.join
295
296
  )
296
297
 
297
- OllamaResponse.new(final_response, model: parameters[:model])
298
+ OllamaResponse.new(final_response, model: model)
298
299
  end
299
300
 
300
301
  # BUG: If streamed, this method does not currently return the tool_calls response.
301
- def generate_final_chat_completion_response(responses_stream, parameters)
302
+ def generate_final_chat_completion_response(responses_stream, model)
302
303
  final_response = responses_stream.last
303
304
  final_response["message"]["content"] = responses_stream.map { |resp| resp.dig("message", "content") }.join
304
305
 
305
- OllamaResponse.new(final_response, model: parameters[:model])
306
+ OllamaResponse.new(final_response, model: model)
306
307
  end
307
308
  end
308
309
  end
@@ -26,8 +26,6 @@ module Langchain::LLM
26
26
  "text-embedding-3-small" => 1536
27
27
  }.freeze
28
28
 
29
- attr_reader :defaults
30
-
31
29
  # Initialize an OpenAI LLM instance
32
30
  #
33
31
  # @param api_key [String] The API key to use
@@ -35,7 +33,11 @@ module Langchain::LLM
35
33
  def initialize(api_key:, llm_options: {}, default_options: {})
36
34
  depends_on "ruby-openai", req: "openai"
37
35
 
38
- @client = ::OpenAI::Client.new(access_token: api_key, **llm_options)
36
+ llm_options[:log_errors] = Langchain.logger.debug? unless llm_options.key?(:log_errors)
37
+
38
+ @client = ::OpenAI::Client.new(access_token: api_key, **llm_options) do |f|
39
+ f.response :logger, Langchain.logger, {headers: true, bodies: true, errors: true}
40
+ end
39
41
 
40
42
  @defaults = DEFAULTS.merge(default_options)
41
43
  chat_parameters.update(
@@ -44,7 +46,8 @@ module Langchain::LLM
44
46
  top_logprobs: {},
45
47
  n: {default: @defaults[:n]},
46
48
  temperature: {default: @defaults[:temperature]},
47
- user: {}
49
+ user: {},
50
+ response_format: {default: @defaults[:response_format]}
48
51
  )
49
52
  chat_parameters.ignore(:top_k)
50
53
  end
@@ -122,11 +125,11 @@ module Langchain::LLM
122
125
  raise ArgumentError.new("'tool_choice' is only allowed when 'tools' are specified.")
123
126
  end
124
127
 
125
- # TODO: Clean this part up
126
128
  if block
127
129
  @response_chunks = []
130
+ parameters[:stream_options] = {include_usage: true}
128
131
  parameters[:stream] = proc do |chunk, _bytesize|
129
- chunk_content = chunk.dig("choices", 0)
132
+ chunk_content = chunk.dig("choices", 0) || {}
130
133
  @response_chunks << chunk
131
134
  yield chunk_content
132
135
  end
@@ -177,7 +180,9 @@ module Langchain::LLM
177
180
  end
178
181
 
179
182
  def response_from_chunks
180
- grouped_chunks = @response_chunks.group_by { |chunk| chunk.dig("choices", 0, "index") }
183
+ grouped_chunks = @response_chunks
184
+ .group_by { |chunk| chunk.dig("choices", 0, "index") }
185
+ .except(nil) # the last chunk (that contains the token usage) has no index
181
186
  final_choices = grouped_chunks.map do |index, chunks|
182
187
  {
183
188
  "index" => index,
@@ -189,7 +194,7 @@ module Langchain::LLM
189
194
  "finish_reason" => chunks.last.dig("choices", 0, "finish_reason")
190
195
  }
191
196
  end
192
- @response_chunks.first&.slice("id", "object", "created", "model")&.merge({"choices" => final_choices})
197
+ @response_chunks.first&.slice("id", "object", "created", "model")&.merge({"choices" => final_choices, "usage" => @response_chunks.last["usage"]})
193
198
  end
194
199
 
195
200
  def tool_calls_from_choice_chunks(choice_chunks)
@@ -79,7 +79,7 @@ module Langchain::Prompt
79
79
  def load_from_config(config)
80
80
  # If `_type` key is not present in the configuration hash, add it with a default value of `prompt`
81
81
  unless config.key?("_type")
82
- Langchain.logger.warn "No `_type` key found, defaulting to `prompt`"
82
+ Langchain.logger.warn("#{self.class} - No `_type` key found, defaulting to `prompt`")
83
83
  config["_type"] = "prompt"
84
84
  end
85
85
 
@@ -28,7 +28,7 @@ module Langchain::Tool
28
28
  # @param input [String] math expression
29
29
  # @return [String] Answer
30
30
  def execute(input:)
31
- Langchain.logger.info("Executing \"#{input}\"", for: self.class)
31
+ Langchain.logger.debug("#{self.class} - Executing \"#{input}\"")
32
32
 
33
33
  Eqn::Calculator.calc(input)
34
34
  rescue Eqn::ParseError, Eqn::NoVariableValueError
@@ -61,7 +61,7 @@ module Langchain::Tool
61
61
  def describe_tables(tables: [])
62
62
  return "No tables specified" if tables.empty?
63
63
 
64
- Langchain.logger.info("Describing tables: #{tables}", for: self.class)
64
+ Langchain.logger.debug("#{self.class} - Describing tables: #{tables}")
65
65
 
66
66
  tables
67
67
  .map do |table|
@@ -74,7 +74,7 @@ module Langchain::Tool
74
74
  #
75
75
  # @return [String] Database schema
76
76
  def dump_schema
77
- Langchain.logger.info("Dumping schema tables and keys", for: self.class)
77
+ Langchain.logger.debug("#{self.class} - Dumping schema tables and keys")
78
78
 
79
79
  schemas = db.tables.map do |table|
80
80
  describe_table(table)
@@ -87,11 +87,11 @@ module Langchain::Tool
87
87
  # @param input [String] SQL query to be executed
88
88
  # @return [Array] Results from the SQL query
89
89
  def execute(input:)
90
- Langchain.logger.info("Executing \"#{input}\"", for: self.class)
90
+ Langchain.logger.debug("#{self.class} - Executing \"#{input}\"")
91
91
 
92
92
  db[input].to_a
93
93
  rescue Sequel::DatabaseError => e
94
- Langchain.logger.error(e.message, for: self.class)
94
+ Langchain.logger.error("#{self.class} - #{e.message}")
95
95
  e.message # Return error to LLM
96
96
  end
97
97
 
@@ -38,7 +38,7 @@ module Langchain::Tool
38
38
  # @param input [String] search query
39
39
  # @return [String] Answer
40
40
  def execute(input:)
41
- Langchain.logger.info("Executing \"#{input}\"", for: self.class)
41
+ Langchain.logger.debug("#{self.class} - Executing \"#{input}\"")
42
42
 
43
43
  results = execute_search(input: input)
44
44
 
@@ -71,7 +71,7 @@ module Langchain::Tool
71
71
  page_size: 5, # The API default is 20 but that's too many.
72
72
  page: nil
73
73
  )
74
- Langchain.logger.info("Retrieving all news", for: self.class)
74
+ Langchain.logger.debug("#{self.class} - Retrieving all news")
75
75
 
76
76
  params = {apiKey: @api_key}
77
77
  params[:q] = q if q
@@ -107,7 +107,7 @@ module Langchain::Tool
107
107
  page_size: 5,
108
108
  page: nil
109
109
  )
110
- Langchain.logger.info("Retrieving top news headlines", for: self.class)
110
+ Langchain.logger.debug("#{self.class} - Retrieving top news headlines")
111
111
 
112
112
  params = {apiKey: @api_key}
113
113
  params[:country] = country if country
@@ -132,7 +132,7 @@ module Langchain::Tool
132
132
  language: nil,
133
133
  country: nil
134
134
  )
135
- Langchain.logger.info("Retrieving news sources", for: self.class)
135
+ Langchain.logger.debug("#{self.class} - Retrieving news sources")
136
136
 
137
137
  params = {apiKey: @api_key}
138
138
  params[:country] = country if country
@@ -29,7 +29,7 @@ module Langchain::Tool
29
29
  # @param input [String] ruby code expression
30
30
  # @return [String] Answer
31
31
  def execute(input:)
32
- Langchain.logger.info("Executing \"#{input}\"", for: self.class)
32
+ Langchain.logger.debug("#{self.class} - Executing \"#{input}\"")
33
33
 
34
34
  safe_eval(input)
35
35
  end
@@ -44,7 +44,7 @@ module Langchain::Tool
44
44
  def get_current_weather(city:, state_code:, country_code: nil, units: "imperial")
45
45
  validate_input(city: city, state_code: state_code, country_code: country_code, units: units)
46
46
 
47
- Langchain.logger.info("get_current_weather", for: self.class, city:, state_code:, country_code:, units:)
47
+ Langchain.logger.debug("#{self.class} - get_current_weather #{{city:, state_code:, country_code:, units:}}")
48
48
 
49
49
  fetch_current_weather(city: city, state_code: state_code, country_code: country_code, units: units)
50
50
  end
@@ -74,9 +74,9 @@ module Langchain::Tool
74
74
  request = Net::HTTP::Get.new(uri.request_uri)
75
75
  request["Content-Type"] = "application/json"
76
76
 
77
- Langchain.logger.info("Sending request to OpenWeatherMap API", path: path, params: params.except(:appid))
77
+ Langchain.logger.debug("#{self.class} - Sending request to OpenWeatherMap API #{{path: path, params: params.except(:appid)}}")
78
78
  response = http.request(request)
79
- Langchain.logger.info("Received response from OpenWeatherMap API", status: response.code)
79
+ Langchain.logger.debug("#{self.class} - Received response from OpenWeatherMap API #{{status: response.code}}")
80
80
 
81
81
  if response.code == "200"
82
82
  JSON.parse(response.body)
@@ -29,7 +29,7 @@ module Langchain::Tool
29
29
  # @param input [String] search query
30
30
  # @return [String] Answer
31
31
  def execute(input:)
32
- Langchain.logger.info("Executing \"#{input}\"", for: self.class)
32
+ Langchain.logger.debug("#{self.class} - Executing \"#{input}\"")
33
33
 
34
34
  page = ::Wikipedia.find(input)
35
35
  # It would be nice to figure out a way to provide page.content but the LLM token limit is an issue
@@ -194,11 +194,5 @@ module Langchain::Vectorsearch
194
194
 
195
195
  add_texts(texts: texts)
196
196
  end
197
-
198
- def self.logger_options
199
- {
200
- color: :blue
201
- }
202
- end
203
197
  end
204
198
  end
@@ -39,7 +39,7 @@ module Langchain::Vectorsearch
39
39
  # This behavior is changed in https://github.com/epsilla-cloud/vectordb/pull/95
40
40
  # Old behavior (HTTP 500) is preserved for backwards compatibility.
41
41
  # It does not prevent us from using the db.
42
- Langchain.logger.info("Database already loaded")
42
+ Langchain.logger.debug("#{self.class} - Database already loaded")
43
43
  else
44
44
  raise "Failed to load database: #{response}"
45
45
  end
@@ -114,12 +114,12 @@ module Langchain::Vectorsearch
114
114
  if File.exist?(path_to_index)
115
115
  client.load_index(path_to_index)
116
116
 
117
- Langchain.logger.info("Successfully loaded the index at \"#{path_to_index}\"", for: self.class)
117
+ Langchain.logger.debug("#{self.class} - Successfully loaded the index at \"#{path_to_index}\"")
118
118
  else
119
119
  # Default max_elements: 100, but we constantly resize the index as new data is written to it
120
120
  client.init_index(max_elements: 100)
121
121
 
122
- Langchain.logger.info("Creating a new index at \"#{path_to_index}\"", for: self.class)
122
+ Langchain.logger.debug("#{self.class} - Creating a new index at \"#{path_to_index}\"")
123
123
  end
124
124
  end
125
125
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Langchain
4
- VERSION = "0.16.0"
4
+ VERSION = "0.16.1"
5
5
  end
data/lib/langchain.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "logger"
4
4
  require "pathname"
5
- require "rainbow"
6
5
  require "zeitwerk"
7
6
  require "uri"
8
7
  require "json"
@@ -92,24 +91,58 @@ loader.setup
92
91
  # Langchain.logger.level = :info
93
92
  module Langchain
94
93
  class << self
95
- # @return [ContextualLogger]
96
- attr_reader :logger
97
-
98
- # @param logger [Logger]
99
- # @return [ContextualLogger]
100
- def logger=(logger)
101
- @logger = ContextualLogger.new(logger)
102
- end
103
-
94
+ # @return [Logger]
95
+ attr_accessor :logger
104
96
  # @return [Pathname]
105
97
  attr_reader :root
106
98
  end
107
99
 
108
- self.logger ||= ::Logger.new($stdout, level: :debug)
109
-
110
- @root = Pathname.new(__dir__)
111
-
112
100
  module Errors
113
101
  class BaseError < StandardError; end
114
102
  end
103
+
104
+ module Colorizer
105
+ class << self
106
+ def red(str)
107
+ "\e[31m#{str}\e[0m"
108
+ end
109
+
110
+ def green(str)
111
+ "\e[32m#{str}\e[0m"
112
+ end
113
+
114
+ def yellow(str)
115
+ "\e[33m#{str}\e[0m"
116
+ end
117
+
118
+ def blue(str)
119
+ "\e[34m#{str}\e[0m"
120
+ end
121
+
122
+ def colorize_logger_msg(msg, severity)
123
+ return msg unless msg.is_a?(String)
124
+
125
+ return red(msg) if severity.to_sym == :ERROR
126
+ return yellow(msg) if severity.to_sym == :WARN
127
+ msg
128
+ end
129
+ end
130
+ end
131
+
132
+ LOGGER_OPTIONS = {
133
+ progname: "Langchain.rb",
134
+
135
+ formatter: ->(severity, time, progname, msg) do
136
+ Logger::Formatter.new.call(
137
+ severity,
138
+ time,
139
+ "[#{progname}]",
140
+ Colorizer.colorize_logger_msg(msg, severity)
141
+ )
142
+ end
143
+ }.freeze
144
+
145
+ self.logger ||= ::Logger.new($stdout, **LOGGER_OPTIONS)
146
+
147
+ @root = Pathname.new(__dir__)
115
148
  end
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.16.0
4
+ version: 0.16.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Bondarev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-19 00:00:00.000000000 Z
11
+ date: 2024-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: baran
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.1.9
27
- - !ruby/object:Gem::Dependency
28
- name: rainbow
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: 3.1.0
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: 3.1.0
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: json-schema
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -680,7 +666,6 @@ files:
680
666
  - lib/langchain/chunker/semantic.rb
681
667
  - lib/langchain/chunker/sentence.rb
682
668
  - lib/langchain/chunker/text.rb
683
- - lib/langchain/contextual_logger.rb
684
669
  - lib/langchain/data.rb
685
670
  - lib/langchain/dependency_helper.rb
686
671
  - lib/langchain/evals/ragas/answer_relevance.rb
@@ -758,7 +743,6 @@ files:
758
743
  - lib/langchain/tool/weather.rb
759
744
  - lib/langchain/tool/wikipedia.rb
760
745
  - lib/langchain/tool_definition.rb
761
- - lib/langchain/utils/colorizer.rb
762
746
  - lib/langchain/utils/cosine_similarity.rb
763
747
  - lib/langchain/utils/hash_transformer.rb
764
748
  - lib/langchain/utils/to_boolean.rb
@@ -799,7 +783,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
799
783
  - !ruby/object:Gem::Version
800
784
  version: '0'
801
785
  requirements: []
802
- rubygems_version: 3.5.11
786
+ rubygems_version: 3.5.20
803
787
  signing_key:
804
788
  specification_version: 4
805
789
  summary: Build LLM-backed Ruby applications with Ruby's Langchain.rb
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Langchain
4
- class ContextualLogger
5
- MESSAGE_COLOR_OPTIONS = {
6
- debug: {
7
- color: :white
8
- },
9
- error: {
10
- color: :red
11
- },
12
- fatal: {
13
- color: :red,
14
- background: :white,
15
- mode: :bold
16
- },
17
- unknown: {
18
- color: :white
19
- },
20
- info: {
21
- color: :white
22
- },
23
- warn: {
24
- color: :yellow,
25
- mode: :bold
26
- }
27
- }
28
-
29
- def initialize(logger)
30
- @logger = logger
31
- @levels = Logger::Severity.constants.map(&:downcase)
32
- end
33
-
34
- def respond_to_missing?(method, include_private = false)
35
- @logger.respond_to?(method, include_private)
36
- end
37
-
38
- def method_missing(method, *args, **kwargs, &block)
39
- return @logger.send(method, *args, **kwargs, &block) unless @levels.include?(method)
40
-
41
- for_class = kwargs.delete(:for)
42
- for_class_name = for_class&.name
43
-
44
- log_line_parts = []
45
- log_line_parts << colorize("[Langchain.rb]", color: :yellow)
46
- log_line_parts << if for_class.respond_to?(:logger_options)
47
- colorize("[#{for_class_name}]", for_class.logger_options) + ":"
48
- elsif for_class_name
49
- "[#{for_class_name}]:"
50
- end
51
- log_line_parts << colorize(args.first, MESSAGE_COLOR_OPTIONS[method])
52
- log_line_parts << kwargs if !!kwargs && kwargs.any?
53
- log_line_parts << block.call if block
54
- log_line = log_line_parts.compact.join(" ")
55
-
56
- @logger.send(
57
- method,
58
- log_line
59
- )
60
- end
61
-
62
- private
63
-
64
- def colorize(line, options)
65
- Langchain::Utils::Colorizer.colorize(line, options)
66
- end
67
- end
68
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Langchain
4
- module Utils
5
- class Colorizer
6
- def self.colorize(line, options)
7
- decorated_line = Rainbow(line)
8
- options.each_pair.each do |modifier, value|
9
- decorated_line = if modifier == :mode
10
- decorated_line.public_send(value)
11
- else
12
- decorated_line.public_send(modifier, value)
13
- end
14
- end
15
- decorated_line
16
- end
17
- end
18
- end
19
- end