langchainrb 0.13.4 → 0.14.0

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: 7877086b6c4d0bba6c1fc4cafc156ff476ad83eb30df1a39b279885b224bf35d
4
- data.tar.gz: 3a22b060896725308c5ce137ee5617a44a75355cfc7878ce6080e6b200722c51
3
+ metadata.gz: 68900cd116cf0fb1b77376a4906e5551f0d578ee2bb47c7ec86d32bf44f84e33
4
+ data.tar.gz: f68782c3cdc856799778618d78b6411a85b0c69adf6a4d33489b8025fdca3dce
5
5
  SHA512:
6
- metadata.gz: 05606b99693c0e81f3785a027e155205a3ffce8f4f236868395de837e2bc6f71661c39f6a2cc062e3c0a57a6c8295e13b6910df296a9092f2f7d0596e1c969b0
7
- data.tar.gz: 00d478f82be9984a95a1d11676982dec79dea2a6c0bf92ceb1ab0e3309edac2ecee115a3dd46ca060f040e44d9b7c8623ffd27ba1d5fcd8182832f933eaf2815
6
+ metadata.gz: 158410fd769caaf9074eddc1143ddee9256ac5a466a510c32b74d337eba62fab80b676661cbf1673604d236014a5cb4defdd4743e71abb713a659ddea0fe5e8c
7
+ data.tar.gz: 2e956356a443ff37ad711f6c42f8c4940925bcee4be075b403c78c3f702b487c12790dca9ba7d68a01acaf1c245b2910650b3f938e80cedd1fc2d5af14f7ffa8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.14.0] - 2024-07-12
4
+ - Removed TokenLength validators
5
+ - Assistant works with a Mistral LLM now
6
+ - Assistant keeps track of tokens used
7
+ - Misc fixes and improvements
8
+
9
+ ## [0.13.5] - 2024-07-01
10
+ - Add Milvus#remove_texts() method
11
+ - Langchain::Assistant has a `state` now
12
+ - Misc fixes and improvements
13
+
3
14
  ## [0.13.4] - 2024-06-16
4
15
  - Fix Chroma#remove_texts() method
5
16
  - Fix NewsRetriever Tool returning non UTF-8 characters
data/README.md CHANGED
@@ -343,7 +343,7 @@ You can instantiate any other supported vector search database:
343
343
  client = Langchain::Vectorsearch::Chroma.new(...) # `gem "chroma-db", "~> 0.6.0"`
344
344
  client = Langchain::Vectorsearch::Epsilla.new(...) # `gem "epsilla-ruby", "~> 0.0.3"`
345
345
  client = Langchain::Vectorsearch::Hnswlib.new(...) # `gem "hnswlib", "~> 0.8.1"`
346
- client = Langchain::Vectorsearch::Milvus.new(...) # `gem "milvus", "~> 0.9.2"`
346
+ client = Langchain::Vectorsearch::Milvus.new(...) # `gem "milvus", "~> 0.9.3"`
347
347
  client = Langchain::Vectorsearch::Pinecone.new(...) # `gem "pinecone", "~> 0.1.6"`
348
348
  client = Langchain::Vectorsearch::Pgvector.new(...) # `gem "pgvector", "~> 0.2"`
349
349
  client = Langchain::Vectorsearch::Qdrant.new(...) # `gem "qdrant-ruby", "~> 0.9.3"`
@@ -428,25 +428,10 @@ Assistants are Agent-like objects that leverage helpful instructions, LLMs, tool
428
428
  ```ruby
429
429
  llm = Langchain::LLM::OpenAI.new(api_key: ENV["OPENAI_API_KEY"])
430
430
  ```
431
- 2. Instantiate a Thread. Threads keep track of the messages in the Assistant conversation.
432
- ```ruby
433
- thread = Langchain::Thread.new
434
- ```
435
- You can pass old message from previously using the Assistant:
436
- ```ruby
437
- thread.messages = messages
438
- ```
439
- Messages contain the conversation history and the whole message history is sent to the LLM every time. A Message belongs to 1 of the 4 roles:
440
- * `Message(role: "system")` message usually contains the instructions.
441
- * `Message(role: "user")` messages come from the user.
442
- * `Message(role: "assistant")` messages are produced by the LLM.
443
- * `Message(role: "tool")` messages are sent in response to tool calls with tool outputs.
444
-
445
- 3. Instantiate an Assistant
431
+ 2. Instantiate an Assistant
446
432
  ```ruby
447
433
  assistant = Langchain::Assistant.new(
448
434
  llm: llm,
449
- thread: thread,
450
435
  instructions: "You are a Meteorologist Assistant that is able to pull the weather for any location",
451
436
  tools: [
452
437
  Langchain::Tool::Weather.new(api_key: ENV["OPEN_WEATHER_API_KEY"])
@@ -482,7 +467,7 @@ assistant.add_message_and_run content: "What about Sacramento, CA?", auto_tool_e
482
467
  ### Accessing Thread messages
483
468
  You can access the messages in a Thread by calling `assistant.thread.messages`.
484
469
  ```ruby
485
- assistant.thread.messages
470
+ assistant.messages
486
471
  ```
487
472
 
488
473
  The Assistant checks the context window limits before every request to the LLM and remove oldest thread messages one by one if the context window is exceeded.
@@ -15,14 +15,16 @@ module Langchain
15
15
  extend Forwardable
16
16
  def_delegators :thread, :messages, :messages=
17
17
 
18
- attr_reader :llm, :thread, :instructions
18
+ attr_reader :llm, :thread, :instructions, :state
19
+ attr_reader :total_prompt_tokens, :total_completion_tokens, :total_tokens
19
20
  attr_accessor :tools
20
21
 
21
22
  SUPPORTED_LLMS = [
22
23
  Langchain::LLM::Anthropic,
23
- Langchain::LLM::OpenAI,
24
24
  Langchain::LLM::GoogleGemini,
25
- Langchain::LLM::GoogleVertexAI
25
+ Langchain::LLM::GoogleVertexAI,
26
+ Langchain::LLM::Ollama,
27
+ Langchain::LLM::OpenAI
26
28
  ]
27
29
 
28
30
  # Create a new assistant
@@ -40,20 +42,26 @@ module Langchain
40
42
  unless SUPPORTED_LLMS.include?(llm.class)
41
43
  raise ArgumentError, "Invalid LLM; currently only #{SUPPORTED_LLMS.join(", ")} are supported"
42
44
  end
45
+ if llm.is_a?(Langchain::LLM::Ollama)
46
+ raise ArgumentError, "Currently only `mistral:7b-instruct-v0.3-fp16` model is supported for Ollama LLM" unless llm.defaults[:completion_model_name] == "mistral:7b-instruct-v0.3-fp16"
47
+ end
43
48
  raise ArgumentError, "Tools must be an array of Langchain::Tool::Base instance(s)" unless tools.is_a?(Array) && tools.all? { |tool| tool.is_a?(Langchain::Tool::Base) }
44
49
 
45
50
  @llm = llm
46
51
  @thread = thread || Langchain::Thread.new
47
52
  @tools = tools
48
53
  @instructions = instructions
54
+ @state = :ready
55
+
56
+ @total_prompt_tokens = 0
57
+ @total_completion_tokens = 0
58
+ @total_tokens = 0
49
59
 
50
60
  raise ArgumentError, "Thread must be an instance of Langchain::Thread" unless @thread.is_a?(Langchain::Thread)
51
61
 
52
62
  # The first message in the thread should be the system instructions
53
63
  # TODO: What if the user added old messages and the system instructions are already in there? Should this overwrite the existing instructions?
54
- if llm.is_a?(Langchain::LLM::OpenAI)
55
- add_message(role: "system", content: instructions) if instructions
56
- end
64
+ initialize_instructions
57
65
  # For Google Gemini, and Anthropic system instructions are added to the `system:` param in the `chat` method
58
66
  end
59
67
 
@@ -66,7 +74,10 @@ module Langchain
66
74
  # @return [Array<Langchain::Message>] The messages in the thread
67
75
  def add_message(content: nil, role: "user", tool_calls: [], tool_call_id: nil)
68
76
  message = build_message(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
69
- thread.add_message(message)
77
+ messages = thread.add_message(message)
78
+ @state = :ready
79
+
80
+ messages
70
81
  end
71
82
 
72
83
  # Run the assistant
@@ -76,56 +87,12 @@ module Langchain
76
87
  def run(auto_tool_execution: false)
77
88
  if thread.messages.empty?
78
89
  Langchain.logger.warn("No messages in the thread")
90
+ @state = :completed
79
91
  return
80
92
  end
81
93
 
82
- running = true
83
-
84
- while running
85
- # TODO: I think we need to look at all messages and not just the last one.
86
- last_message = thread.messages.last
87
-
88
- if last_message.system?
89
- # Do nothing
90
- running = false
91
- elsif last_message.llm?
92
- if last_message.tool_calls.any?
93
- if auto_tool_execution
94
- run_tools(last_message.tool_calls)
95
- else
96
- # Maybe log and tell the user that there's outstanding tool calls?
97
- running = false
98
- end
99
- else
100
- # Last message was from the assistant without any tools calls.
101
- # Do nothing
102
- running = false
103
- end
104
- elsif last_message.user?
105
- # Run it!
106
- response = chat_with_llm
107
-
108
- if response.tool_calls.any?
109
- # Re-run the while(running) loop to process the tool calls
110
- running = true
111
- add_message(role: response.role, tool_calls: response.tool_calls)
112
- elsif response.chat_completion
113
- # Stop the while(running) loop and add the assistant's response to the thread
114
- running = false
115
- add_message(role: response.role, content: response.chat_completion)
116
- end
117
- elsif last_message.tool?
118
- # Run it!
119
- response = chat_with_llm
120
- running = true
121
-
122
- if response.tool_calls.any?
123
- add_message(role: response.role, tool_calls: response.tool_calls)
124
- elsif response.chat_completion
125
- add_message(role: response.role, content: response.chat_completion)
126
- end
127
- end
128
- end
94
+ @state = :in_progress
95
+ @state = handle_state until run_finished?(auto_tool_execution)
129
96
 
130
97
  thread.messages
131
98
  end
@@ -146,13 +113,7 @@ module Langchain
146
113
  # @param output [String] The output of the tool
147
114
  # @return [Array<Langchain::Message>] The messages in the thread
148
115
  def submit_tool_output(tool_call_id:, output:)
149
- tool_role = if llm.is_a?(Langchain::LLM::OpenAI)
150
- Langchain::Messages::OpenAIMessage::TOOL_ROLE
151
- elsif [Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI].include?(llm.class)
152
- Langchain::Messages::GoogleGeminiMessage::TOOL_ROLE
153
- elsif llm.is_a?(Langchain::LLM::Anthropic)
154
- Langchain::Messages::AnthropicMessage::TOOL_ROLE
155
- end
116
+ tool_role = determine_tool_role
156
117
 
157
118
  # TODO: Validate that `tool_call_id` is valid by scanning messages and checking if this tool call ID was invoked
158
119
  add_message(role: tool_role, content: output, tool_call_id: tool_call_id)
@@ -183,31 +144,181 @@ module Langchain
183
144
 
184
145
  private
185
146
 
147
+ # Check if the run is finished
148
+ #
149
+ # @param auto_tool_execution [Boolean] Whether or not to automatically run tools
150
+ # @return [Boolean] Whether the run is finished
151
+ def run_finished?(auto_tool_execution)
152
+ finished_states = [:completed, :failed]
153
+
154
+ requires_manual_action = (@state == :requires_action) && !auto_tool_execution
155
+ finished_states.include?(@state) || requires_manual_action
156
+ end
157
+
158
+ # Handle the current state and transition to the next state
159
+ #
160
+ # @return [Symbol] The next state
161
+ def handle_state
162
+ case @state
163
+ when :in_progress
164
+ process_latest_message
165
+ when :requires_action
166
+ execute_tools
167
+ end
168
+ end
169
+
170
+ # Process the latest message in the thread
171
+ #
172
+ # @return [Symbol] The next state
173
+ def process_latest_message
174
+ last_message = thread.messages.last
175
+
176
+ case last_message.standard_role
177
+ when :system
178
+ handle_system_message
179
+ when :llm
180
+ handle_llm_message
181
+ when :user, :tool
182
+ handle_user_or_tool_message
183
+ else
184
+ handle_unexpected_message
185
+ end
186
+ end
187
+
188
+ # Handle system message scenario
189
+ #
190
+ # @return [Symbol] The completed state
191
+ def handle_system_message
192
+ Langchain.logger.warn("At least one user message is required after a system message")
193
+ :completed
194
+ end
195
+
196
+ # Handle LLM message scenario
197
+ #
198
+ # @return [Symbol] The next state
199
+ def handle_llm_message
200
+ thread.messages.last.tool_calls.any? ? :requires_action : :completed
201
+ end
202
+
203
+ # Handle unexpected message scenario
204
+ #
205
+ # @return [Symbol] The failed state
206
+ def handle_unexpected_message
207
+ Langchain.logger.error("Unexpected message role encountered: #{thread.messages.last.standard_role}")
208
+ :failed
209
+ end
210
+
211
+ # Handle user or tool message scenario by processing the LLM response
212
+ #
213
+ # @return [Symbol] The next state
214
+ def handle_user_or_tool_message
215
+ response = chat_with_llm
216
+
217
+ # With Ollama, we're calling the `llm.complete()` method
218
+ content = if llm.is_a?(Langchain::LLM::Ollama)
219
+ response.completion
220
+ else
221
+ response.chat_completion
222
+ end
223
+
224
+ add_message(role: response.role, content: content, tool_calls: response.tool_calls)
225
+ record_used_tokens(response.prompt_tokens, response.completion_tokens, response.total_tokens)
226
+
227
+ set_state_for(response: response)
228
+ end
229
+
230
+ def set_state_for(response:)
231
+ if response.tool_calls.any?
232
+ :in_progress
233
+ elsif response.chat_completion
234
+ :completed
235
+ elsif response.completion # Currently only used by Ollama
236
+ :completed
237
+ else
238
+ Langchain.logger.error("LLM response does not contain tool calls, chat or completion response")
239
+ :failed
240
+ end
241
+ end
242
+
243
+ # Execute the tools based on the tool calls in the last message
244
+ #
245
+ # @return [Symbol] The next state
246
+ def execute_tools
247
+ run_tools(thread.messages.last.tool_calls)
248
+ :in_progress
249
+ rescue => e
250
+ Langchain.logger.error("Error running tools: #{e.message}")
251
+ :failed
252
+ end
253
+
254
+ # Determine the tool role based on the LLM type
255
+ #
256
+ # @return [String] The tool role
257
+ def determine_tool_role
258
+ case llm
259
+ when Langchain::LLM::Ollama
260
+ Langchain::Messages::OllamaMessage::TOOL_ROLE
261
+ when Langchain::LLM::OpenAI
262
+ Langchain::Messages::OpenAIMessage::TOOL_ROLE
263
+ when Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI
264
+ Langchain::Messages::GoogleGeminiMessage::TOOL_ROLE
265
+ when Langchain::LLM::Anthropic
266
+ Langchain::Messages::AnthropicMessage::TOOL_ROLE
267
+ end
268
+ end
269
+
270
+ def initialize_instructions
271
+ if llm.is_a?(Langchain::LLM::Ollama)
272
+ content = String.new # rubocop: disable Performance/UnfreezeString
273
+ if tools.any?
274
+ content << %([AVAILABLE_TOOLS] #{tools.map(&:to_openai_tools).flatten}[/AVAILABLE_TOOLS])
275
+ end
276
+ if instructions
277
+ content << "[INST] #{instructions}[/INST]"
278
+ end
279
+
280
+ add_message(role: "system", content: content)
281
+ elsif llm.is_a?(Langchain::LLM::OpenAI)
282
+ add_message(role: "system", content: instructions) if instructions
283
+ end
284
+ end
285
+
186
286
  # Call to the LLM#chat() method
187
287
  #
188
288
  # @return [Langchain::LLM::BaseResponse] The LLM response object
189
289
  def chat_with_llm
190
290
  Langchain.logger.info("Sending a call to #{llm.class}", for: self.class)
191
291
 
192
- params = {messages: thread.array_of_message_hashes}
292
+ params = {}
193
293
 
194
- if tools.any?
195
- if llm.is_a?(Langchain::LLM::OpenAI)
294
+ if llm.is_a?(Langchain::LLM::OpenAI)
295
+ if tools.any?
196
296
  params[:tools] = tools.map(&:to_openai_tools).flatten
197
297
  params[:tool_choice] = "auto"
198
- elsif llm.is_a?(Langchain::LLM::Anthropic)
298
+ end
299
+ elsif llm.is_a?(Langchain::LLM::Anthropic)
300
+ if tools.any?
199
301
  params[:tools] = tools.map(&:to_anthropic_tools).flatten
200
- params[:system] = instructions if instructions
201
302
  params[:tool_choice] = {type: "auto"}
202
- elsif [Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI].include?(llm.class)
303
+ end
304
+ params[:system] = instructions if instructions
305
+ elsif [Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI].include?(llm.class)
306
+ if tools.any?
203
307
  params[:tools] = tools.map(&:to_google_gemini_tools).flatten
204
308
  params[:system] = instructions if instructions
205
309
  params[:tool_choice] = "auto"
206
310
  end
207
- # TODO: Not sure that tool_choice should always be "auto"; Maybe we can let the user toggle it.
208
311
  end
209
-
210
- llm.chat(**params)
312
+ # TODO: Not sure that tool_choice should always be "auto"; Maybe we can let the user toggle it.
313
+
314
+ if llm.is_a?(Langchain::LLM::Ollama)
315
+ params[:raw] = true
316
+ params[:prompt] = thread.prompt_of_concatenated_messages
317
+ llm.complete(**params)
318
+ else
319
+ params[:messages] = thread.array_of_message_hashes
320
+ llm.chat(**params)
321
+ end
211
322
  end
212
323
 
213
324
  # Run the tools automatically
@@ -216,7 +327,9 @@ module Langchain
216
327
  def run_tools(tool_calls)
217
328
  # Iterate over each function invocation and submit tool output
218
329
  tool_calls.each do |tool_call|
219
- tool_call_id, tool_name, method_name, tool_arguments = if llm.is_a?(Langchain::LLM::OpenAI)
330
+ tool_call_id, tool_name, method_name, tool_arguments = if llm.is_a?(Langchain::LLM::Ollama)
331
+ extract_ollama_tool_call(tool_call: tool_call)
332
+ elsif llm.is_a?(Langchain::LLM::OpenAI)
220
333
  extract_openai_tool_call(tool_call: tool_call)
221
334
  elsif [Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI].include?(llm.class)
222
335
  extract_google_gemini_tool_call(tool_call: tool_call)
@@ -232,14 +345,12 @@ module Langchain
232
345
 
233
346
  submit_tool_output(tool_call_id: tool_call_id, output: output)
234
347
  end
348
+ end
235
349
 
236
- response = chat_with_llm
237
-
238
- if response.tool_calls.any?
239
- add_message(role: response.role, tool_calls: response.tool_calls)
240
- elsif response.chat_completion
241
- add_message(role: response.role, content: response.chat_completion)
242
- end
350
+ def extract_ollama_tool_call(tool_call:)
351
+ tool_name, method_name = tool_call.dig("name").split("__")
352
+ tool_arguments = tool_call.dig("arguments").transform_keys(&:to_sym)
353
+ [nil, tool_name, method_name, tool_arguments]
243
354
  end
244
355
 
245
356
  # Extract the tool call information from the OpenAI tool call hash
@@ -292,7 +403,9 @@ module Langchain
292
403
  # @param tool_call_id [String] The ID of the tool call to include in the message
293
404
  # @return [Langchain::Message] The Message object
294
405
  def build_message(role:, content: nil, tool_calls: [], tool_call_id: nil)
295
- if llm.is_a?(Langchain::LLM::OpenAI)
406
+ if llm.is_a?(Langchain::LLM::Ollama)
407
+ Langchain::Messages::OllamaMessage.new(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
408
+ elsif llm.is_a?(Langchain::LLM::OpenAI)
296
409
  Langchain::Messages::OpenAIMessage.new(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
297
410
  elsif [Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI].include?(llm.class)
298
411
  Langchain::Messages::GoogleGeminiMessage.new(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
@@ -301,6 +414,18 @@ module Langchain
301
414
  end
302
415
  end
303
416
 
417
+ # Increment the tokens count based on the last interaction with the LLM
418
+ #
419
+ # @param prompt_tokens [Integer] The number of used prmopt tokens
420
+ # @param completion_tokens [Integer] The number of used completion tokens
421
+ # @param total_tokens [Integer] The total number of used tokens
422
+ # @return [Integer] The current total tokens count
423
+ def record_used_tokens(prompt_tokens, completion_tokens, total_tokens_from_operation)
424
+ @total_prompt_tokens += prompt_tokens if prompt_tokens
425
+ @total_completion_tokens += completion_tokens if completion_tokens
426
+ @total_tokens += total_tokens_from_operation if total_tokens_from_operation
427
+ end
428
+
304
429
  # TODO: Fix the message truncation when context window is exceeded
305
430
  end
306
431
  end
@@ -7,10 +7,44 @@ module Langchain
7
7
 
8
8
  # Check if the message came from a user
9
9
  #
10
- # @param [Boolean] true/false whether the message came from a user
10
+ # @return [Boolean] true/false whether the message came from a user
11
11
  def user?
12
12
  role == "user"
13
13
  end
14
+
15
+ # Check if the message came from an LLM
16
+ #
17
+ # @raise NotImplementedError if the subclass does not implement this method
18
+ def llm?
19
+ raise NotImplementedError, "Class #{self.class.name} must implement the method 'llm?'"
20
+ end
21
+
22
+ # Check if the message is a tool call
23
+ #
24
+ # @raise NotImplementedError if the subclass does not implement this method
25
+ def tool?
26
+ raise NotImplementedError, "Class #{self.class.name} must implement the method 'tool?'"
27
+ end
28
+
29
+ # Check if the message is a system prompt
30
+ #
31
+ # @raise NotImplementedError if the subclass does not implement this method
32
+ def system?
33
+ raise NotImplementedError, "Class #{self.class.name} must implement the method 'system?'"
34
+ end
35
+
36
+ # Returns the standardized role symbol based on the specific role methods
37
+ #
38
+ # @return [Symbol] the standardized role symbol (:system, :llm, :tool, :user, or :unknown)
39
+ def standard_role
40
+ return :user if user?
41
+ return :llm if llm?
42
+ return :tool if tool?
43
+ return :system if system?
44
+
45
+ # TODO: Should we return :unknown or raise an error?
46
+ :unknown
47
+ end
14
48
  end
15
49
  end
16
50
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langchain
4
+ module Messages
5
+ class OllamaMessage < Base
6
+ # OpenAI uses the following roles:
7
+ ROLES = [
8
+ "system",
9
+ "assistant",
10
+ "user",
11
+ "tool"
12
+ ].freeze
13
+
14
+ TOOL_ROLE = "tool"
15
+
16
+ # Initialize a new OpenAI message
17
+ #
18
+ # @param [String] The role of the message
19
+ # @param [String] The content of the message
20
+ # @param [Array<Hash>] The tool calls made in the message
21
+ # @param [String] The ID of the tool call
22
+ def initialize(role:, content: nil, tool_calls: [], tool_call_id: nil)
23
+ raise ArgumentError, "Role must be one of #{ROLES.join(", ")}" unless ROLES.include?(role)
24
+ raise ArgumentError, "Tool calls must be an array of hashes" unless tool_calls.is_a?(Array) && tool_calls.all? { |tool_call| tool_call.is_a?(Hash) }
25
+
26
+ @role = role
27
+ # Some Tools return content as a JSON hence `.to_s`
28
+ @content = content.to_s
29
+ @tool_calls = tool_calls
30
+ @tool_call_id = tool_call_id
31
+ end
32
+
33
+ def to_s
34
+ send(:"to_#{role}_message_string")
35
+ end
36
+
37
+ def to_system_message_string
38
+ content
39
+ end
40
+
41
+ def to_user_message_string
42
+ "[INST] #{content}[/INST]"
43
+ end
44
+
45
+ def to_tool_message_string
46
+ "[TOOL_RESULTS] #{content}[/TOOL_RESULTS]"
47
+ end
48
+
49
+ def to_assistant_message_string
50
+ if tool_calls.any?
51
+ %("[TOOL_CALLS] #{tool_calls}")
52
+ else
53
+ content
54
+ end
55
+ end
56
+
57
+ # Check if the message came from an LLM
58
+ #
59
+ # @return [Boolean] true/false whether this message was produced by an LLM
60
+ def llm?
61
+ assistant?
62
+ end
63
+
64
+ # Check if the message came from an LLM
65
+ #
66
+ # @return [Boolean] true/false whether this message was produced by an LLM
67
+ def assistant?
68
+ role == "assistant"
69
+ end
70
+
71
+ # Check if the message are system instructions
72
+ #
73
+ # @return [Boolean] true/false whether this message are system instructions
74
+ def system?
75
+ role == "system"
76
+ end
77
+
78
+ # Check if the message is a tool call
79
+ #
80
+ # @return [Boolean] true/false whether this message is a tool call
81
+ def tool?
82
+ role == "tool"
83
+ end
84
+ end
85
+ end
86
+ end
@@ -17,7 +17,14 @@ module Langchain
17
17
  #
18
18
  # @return [Array<Hash>] The thread as an OpenAI API-compatible array of hashes
19
19
  def array_of_message_hashes
20
- messages.map(&:to_hash)
20
+ messages
21
+ .map(&:to_hash)
22
+ .compact
23
+ end
24
+
25
+ # Only used by the Assistant when it calls the LLM#complete() method
26
+ def prompt_of_concatenated_messages
27
+ messages.map(&:to_s).join
21
28
  end
22
29
 
23
30
  # Add a message to the thread
@@ -16,8 +16,6 @@ module Langchain::LLM
16
16
  model: "j2-ultra"
17
17
  }.freeze
18
18
 
19
- LENGTH_VALIDATOR = Langchain::Utils::TokenLength::AI21Validator
20
-
21
19
  def initialize(api_key:, default_options: {})
22
20
  depends_on "ai21"
23
21
 
@@ -35,8 +33,6 @@ module Langchain::LLM
35
33
  def complete(prompt:, **params)
36
34
  parameters = complete_parameters params
37
35
 
38
- parameters[:maxTokens] = LENGTH_VALIDATOR.validate_max_tokens!(prompt, parameters[:model], {llm: client})
39
-
40
36
  response = client.complete(prompt, parameters)
41
37
  Langchain::LLM::AI21Response.new response, model: parameters[:model]
42
38
  end
@@ -5,10 +5,10 @@ module Langchain::LLM
5
5
  # Wrapper around Anthropic APIs.
6
6
  #
7
7
  # Gem requirements:
8
- # gem "anthropic", "~> 0.1.0"
8
+ # gem "anthropic", "~> 0.3.0"
9
9
  #
10
10
  # Usage:
11
- # anthorpic = Langchain::LLM::Anthropic.new(api_key: ENV["ANTHROPIC_API_KEY"])
11
+ # anthropic = Langchain::LLM::Anthropic.new(api_key: ENV["ANTHROPIC_API_KEY"])
12
12
  #
13
13
  class Anthropic < Base
14
14
  DEFAULTS = {
@@ -18,9 +18,6 @@ module Langchain::LLM
18
18
  max_tokens_to_sample: 256
19
19
  }.freeze
20
20
 
21
- # TODO: Implement token length validator for Anthropic
22
- # LENGTH_VALIDATOR = Langchain::Utils::TokenLength::AnthropicValidator
23
-
24
21
  # Initialize an Anthropic LLM instance
25
22
  #
26
23
  # @param api_key [String] The API key to use
@@ -81,7 +78,10 @@ module Langchain::LLM
81
78
  parameters[:metadata] = metadata if metadata
82
79
  parameters[:stream] = stream if stream
83
80
 
84
- response = client.complete(parameters: parameters)
81
+ response = with_api_error_handling do
82
+ client.complete(parameters: parameters)
83
+ end
84
+
85
85
  Langchain::LLM::AnthropicResponse.new(response)
86
86
  end
87
87
 
@@ -114,6 +114,15 @@ module Langchain::LLM
114
114
  Langchain::LLM::AnthropicResponse.new(response)
115
115
  end
116
116
 
117
+ def with_api_error_handling
118
+ response = yield
119
+ return if response.empty?
120
+
121
+ raise Langchain::LLM::ApiError.new "Anthropic API error: #{response.dig("error", "message")}" if response&.dig("error")
122
+
123
+ response
124
+ end
125
+
117
126
  private
118
127
 
119
128
  def set_extra_headers!