langchainrb 0.16.0 → 0.16.1

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: 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