langchainrb 0.13.5 → 0.15.0
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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +2 -17
- data/lib/langchain/assistants/assistant.rb +207 -92
- data/lib/langchain/assistants/messages/ollama_message.rb +74 -0
- data/lib/langchain/assistants/thread.rb +8 -1
- data/lib/langchain/contextual_logger.rb +2 -2
- data/lib/langchain/llm/ai21.rb +0 -4
- data/lib/langchain/llm/anthropic.rb +15 -6
- data/lib/langchain/llm/azure.rb +3 -3
- data/lib/langchain/llm/base.rb +1 -0
- data/lib/langchain/llm/cohere.rb +0 -2
- data/lib/langchain/llm/google_gemini.rb +1 -1
- data/lib/langchain/llm/google_palm.rb +1 -4
- data/lib/langchain/llm/ollama.rb +24 -18
- data/lib/langchain/llm/openai.rb +1 -1
- data/lib/langchain/llm/response/google_gemini_response.rb +1 -1
- data/lib/langchain/llm/response/ollama_response.rb +5 -1
- data/lib/langchain/llm/unified_parameters.rb +2 -2
- data/lib/langchain/tool/calculator.rb +38 -0
- data/lib/langchain/tool/{database/database.rb → database.rb} +24 -12
- data/lib/langchain/tool/file_system.rb +44 -0
- data/lib/langchain/tool/{google_search/google_search.rb → google_search.rb} +17 -23
- data/lib/langchain/tool/{news_retriever/news_retriever.rb → news_retriever.rb} +41 -14
- data/lib/langchain/tool/ruby_code_interpreter.rb +41 -0
- data/lib/langchain/tool/{tavily/tavily.rb → tavily.rb} +24 -10
- data/lib/langchain/tool/vectorsearch.rb +40 -0
- data/lib/langchain/tool/{weather/weather.rb → weather.rb} +21 -17
- data/lib/langchain/tool/{wikipedia/wikipedia.rb → wikipedia.rb} +17 -13
- data/lib/langchain/tool_definition.rb +212 -0
- data/lib/langchain/utils/hash_transformer.rb +9 -17
- data/lib/langchain/vectorsearch/chroma.rb +2 -2
- data/lib/langchain/vectorsearch/elasticsearch.rb +2 -2
- data/lib/langchain/vectorsearch/epsilla.rb +3 -3
- data/lib/langchain/vectorsearch/milvus.rb +3 -3
- data/lib/langchain/vectorsearch/pgvector.rb +2 -2
- data/lib/langchain/vectorsearch/pinecone.rb +2 -2
- data/lib/langchain/vectorsearch/qdrant.rb +2 -2
- data/lib/langchain/vectorsearch/weaviate.rb +4 -4
- data/lib/langchain/version.rb +1 -1
- metadata +16 -45
- data/lib/langchain/tool/base.rb +0 -107
- data/lib/langchain/tool/calculator/calculator.json +0 -19
- data/lib/langchain/tool/calculator/calculator.rb +0 -34
- data/lib/langchain/tool/database/database.json +0 -46
- data/lib/langchain/tool/file_system/file_system.json +0 -57
- data/lib/langchain/tool/file_system/file_system.rb +0 -32
- data/lib/langchain/tool/google_search/google_search.json +0 -19
- data/lib/langchain/tool/news_retriever/news_retriever.json +0 -122
- data/lib/langchain/tool/ruby_code_interpreter/ruby_code_interpreter.json +0 -19
- data/lib/langchain/tool/ruby_code_interpreter/ruby_code_interpreter.rb +0 -37
- data/lib/langchain/tool/tavily/tavily.json +0 -54
- data/lib/langchain/tool/vectorsearch/vectorsearch.json +0 -24
- data/lib/langchain/tool/vectorsearch/vectorsearch.rb +0 -36
- data/lib/langchain/tool/weather/weather.json +0 -19
- data/lib/langchain/tool/wikipedia/wikipedia.json +0 -19
- data/lib/langchain/utils/token_length/ai21_validator.rb +0 -41
- data/lib/langchain/utils/token_length/base_validator.rb +0 -42
- data/lib/langchain/utils/token_length/cohere_validator.rb +0 -49
- data/lib/langchain/utils/token_length/google_palm_validator.rb +0 -57
- data/lib/langchain/utils/token_length/openai_validator.rb +0 -138
- data/lib/langchain/utils/token_length/token_limit_exceeded.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dde504e05b1cbb32c857569bf71301537fed2deb468f1bdd69a7ef900a41c085
|
4
|
+
data.tar.gz: '08659cddd6f0bb285e167c7a35dbd2f83c2e9bb51a69206217ea91649e99839c'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ce4dd091498659a2d8dda4b54e9e9584dc19be5f390dc5f1d98efa054a264134dc3510f2f83c65bdf23edfbd7344587b91113e69c2ea1fea2cdc157317735799
|
7
|
+
data.tar.gz: a6df110aa7d96c87402164f67aadab0a97e2a62b68b7466cf630fe79dd0611a1740ae11163361eef9c98fc816f7ba12d7bfc0aa2225759cc8191f59fead8fcbd
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.15.0] - 2024-08-14
|
4
|
+
- Fix Langchain::Assistant when llm is Anthropic
|
5
|
+
- Fix GoogleGemini#chat method
|
6
|
+
- Langchain::LLM::Weaviate initializer does not require api_key anymore
|
7
|
+
- [BREAKING] Langchain::LLM::OpenAI#chat() uses `gpt-4o-mini` by default instead of `gpt-3.5-turbo` previously.
|
8
|
+
- [BREAKING] Assistant works with a number of open-source models via Ollama.
|
9
|
+
- [BREAKING] Introduce new `Langchain::ToolDefinition` module to define tools. This replaces the previous reliance on subclassing from `Langchain::Tool::Base`.
|
10
|
+
|
11
|
+
## [0.14.0] - 2024-07-12
|
12
|
+
- Removed TokenLength validators
|
13
|
+
- Assistant works with a Mistral LLM now
|
14
|
+
- Assistant keeps track of tokens used
|
15
|
+
- Misc fixes and improvements
|
16
|
+
|
3
17
|
## [0.13.5] - 2024-07-01
|
4
18
|
- Add Milvus#remove_texts() method
|
5
19
|
- Langchain::Assistant has a `state` now
|
data/README.md
CHANGED
@@ -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
|
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.
|
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.
|
@@ -16,15 +16,9 @@ module Langchain
|
|
16
16
|
def_delegators :thread, :messages, :messages=
|
17
17
|
|
18
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
|
-
SUPPORTED_LLMS = [
|
22
|
-
Langchain::LLM::Anthropic,
|
23
|
-
Langchain::LLM::OpenAI,
|
24
|
-
Langchain::LLM::GoogleGemini,
|
25
|
-
Langchain::LLM::GoogleVertexAI
|
26
|
-
]
|
27
|
-
|
28
22
|
# Create a new assistant
|
29
23
|
#
|
30
24
|
# @param llm [Langchain::LLM::Base] LLM instance that the assistant will use
|
@@ -37,24 +31,26 @@ module Langchain
|
|
37
31
|
tools: [],
|
38
32
|
instructions: nil
|
39
33
|
)
|
40
|
-
unless
|
41
|
-
raise ArgumentError, "
|
34
|
+
unless tools.is_a?(Array) && tools.all? { |tool| tool.class.singleton_class.included_modules.include?(Langchain::ToolDefinition) }
|
35
|
+
raise ArgumentError, "Tools must be an array of objects extending Langchain::ToolDefinition"
|
42
36
|
end
|
43
|
-
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
37
|
|
45
38
|
@llm = llm
|
39
|
+
@llm_adapter = LLM::Adapter.build(llm)
|
46
40
|
@thread = thread || Langchain::Thread.new
|
47
41
|
@tools = tools
|
48
42
|
@instructions = instructions
|
49
43
|
@state = :ready
|
50
44
|
|
45
|
+
@total_prompt_tokens = 0
|
46
|
+
@total_completion_tokens = 0
|
47
|
+
@total_tokens = 0
|
48
|
+
|
51
49
|
raise ArgumentError, "Thread must be an instance of Langchain::Thread" unless @thread.is_a?(Langchain::Thread)
|
52
50
|
|
53
51
|
# The first message in the thread should be the system instructions
|
54
52
|
# TODO: What if the user added old messages and the system instructions are already in there? Should this overwrite the existing instructions?
|
55
|
-
|
56
|
-
add_message(role: "system", content: instructions) if instructions
|
57
|
-
end
|
53
|
+
initialize_instructions
|
58
54
|
# For Google Gemini, and Anthropic system instructions are added to the `system:` param in the `chat` method
|
59
55
|
end
|
60
56
|
|
@@ -150,7 +146,6 @@ module Langchain
|
|
150
146
|
|
151
147
|
# Handle the current state and transition to the next state
|
152
148
|
#
|
153
|
-
# @param state [Symbol] The current state
|
154
149
|
# @return [Symbol] The next state
|
155
150
|
def handle_state
|
156
151
|
case @state
|
@@ -189,7 +184,6 @@ module Langchain
|
|
189
184
|
|
190
185
|
# Handle LLM message scenario
|
191
186
|
#
|
192
|
-
# @param auto_tool_execution [Boolean] Flag to indicate if tools should be executed automatically
|
193
187
|
# @return [Symbol] The next state
|
194
188
|
def handle_llm_message
|
195
189
|
thread.messages.last.tool_calls.any? ? :requires_action : :completed
|
@@ -208,14 +202,22 @@ module Langchain
|
|
208
202
|
# @return [Symbol] The next state
|
209
203
|
def handle_user_or_tool_message
|
210
204
|
response = chat_with_llm
|
205
|
+
|
211
206
|
add_message(role: response.role, content: response.chat_completion, tool_calls: response.tool_calls)
|
207
|
+
record_used_tokens(response.prompt_tokens, response.completion_tokens, response.total_tokens)
|
208
|
+
|
209
|
+
set_state_for(response: response)
|
210
|
+
end
|
212
211
|
|
212
|
+
def set_state_for(response:)
|
213
213
|
if response.tool_calls.any?
|
214
214
|
:in_progress
|
215
215
|
elsif response.chat_completion
|
216
216
|
:completed
|
217
|
+
elsif response.completion # Currently only used by Ollama
|
218
|
+
:completed
|
217
219
|
else
|
218
|
-
Langchain.logger.error("LLM response does not contain tool calls or
|
220
|
+
Langchain.logger.error("LLM response does not contain tool calls, chat or completion response")
|
219
221
|
:failed
|
220
222
|
end
|
221
223
|
end
|
@@ -227,7 +229,7 @@ module Langchain
|
|
227
229
|
run_tools(thread.messages.last.tool_calls)
|
228
230
|
:in_progress
|
229
231
|
rescue => e
|
230
|
-
Langchain.logger.error("Error running tools: #{e.message}")
|
232
|
+
Langchain.logger.error("Error running tools: #{e.message}; #{e.backtrace.join('\n')}")
|
231
233
|
:failed
|
232
234
|
end
|
233
235
|
|
@@ -236,6 +238,8 @@ module Langchain
|
|
236
238
|
# @return [String] The tool role
|
237
239
|
def determine_tool_role
|
238
240
|
case llm
|
241
|
+
when Langchain::LLM::Ollama
|
242
|
+
Langchain::Messages::OllamaMessage::TOOL_ROLE
|
239
243
|
when Langchain::LLM::OpenAI
|
240
244
|
Langchain::Messages::OpenAIMessage::TOOL_ROLE
|
241
245
|
when Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI
|
@@ -245,31 +249,24 @@ module Langchain
|
|
245
249
|
end
|
246
250
|
end
|
247
251
|
|
252
|
+
def initialize_instructions
|
253
|
+
if llm.is_a?(Langchain::LLM::OpenAI)
|
254
|
+
add_message(role: "system", content: instructions) if instructions
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
248
258
|
# Call to the LLM#chat() method
|
249
259
|
#
|
250
260
|
# @return [Langchain::LLM::BaseResponse] The LLM response object
|
251
261
|
def chat_with_llm
|
252
262
|
Langchain.logger.info("Sending a call to #{llm.class}", for: self.class)
|
253
263
|
|
254
|
-
params =
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
elsif llm.is_a?(Langchain::LLM::Anthropic)
|
261
|
-
params[:tools] = tools.map(&:to_anthropic_tools).flatten
|
262
|
-
params[:system] = instructions if instructions
|
263
|
-
params[:tool_choice] = {type: "auto"}
|
264
|
-
elsif [Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI].include?(llm.class)
|
265
|
-
params[:tools] = tools.map(&:to_google_gemini_tools).flatten
|
266
|
-
params[:system] = instructions if instructions
|
267
|
-
params[:tool_choice] = "auto"
|
268
|
-
end
|
269
|
-
# TODO: Not sure that tool_choice should always be "auto"; Maybe we can let the user toggle it.
|
270
|
-
end
|
271
|
-
|
272
|
-
llm.chat(**params)
|
264
|
+
params = @llm_adapter.build_chat_params(
|
265
|
+
tools: @tools,
|
266
|
+
instructions: @instructions,
|
267
|
+
messages: thread.array_of_message_hashes
|
268
|
+
)
|
269
|
+
@llm.chat(**params)
|
273
270
|
end
|
274
271
|
|
275
272
|
# Run the tools automatically
|
@@ -278,16 +275,10 @@ module Langchain
|
|
278
275
|
def run_tools(tool_calls)
|
279
276
|
# Iterate over each function invocation and submit tool output
|
280
277
|
tool_calls.each do |tool_call|
|
281
|
-
tool_call_id, tool_name, method_name, tool_arguments =
|
282
|
-
extract_openai_tool_call(tool_call: tool_call)
|
283
|
-
elsif [Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI].include?(llm.class)
|
284
|
-
extract_google_gemini_tool_call(tool_call: tool_call)
|
285
|
-
elsif llm.is_a?(Langchain::LLM::Anthropic)
|
286
|
-
extract_anthropic_tool_call(tool_call: tool_call)
|
287
|
-
end
|
278
|
+
tool_call_id, tool_name, method_name, tool_arguments = @llm_adapter.extract_tool_call_args(tool_call: tool_call)
|
288
279
|
|
289
280
|
tool_instance = tools.find do |t|
|
290
|
-
t.
|
281
|
+
t.class.tool_name == tool_name
|
291
282
|
end or raise ArgumentError, "Tool not found in assistant.tools"
|
292
283
|
|
293
284
|
output = tool_instance.send(method_name, **tool_arguments)
|
@@ -296,65 +287,189 @@ module Langchain
|
|
296
287
|
end
|
297
288
|
end
|
298
289
|
|
299
|
-
#
|
290
|
+
# Build a message
|
300
291
|
#
|
301
|
-
# @param
|
302
|
-
# @
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
tool_arguments = JSON.parse(tool_call.dig("function", "arguments"), symbolize_names: true)
|
309
|
-
|
310
|
-
[tool_call_id, tool_name, method_name, tool_arguments]
|
292
|
+
# @param role [String] The role of the message
|
293
|
+
# @param content [String] The content of the message
|
294
|
+
# @param tool_calls [Array<Hash>] The tool calls to include in the message
|
295
|
+
# @param tool_call_id [String] The ID of the tool call to include in the message
|
296
|
+
# @return [Langchain::Message] The Message object
|
297
|
+
def build_message(role:, content: nil, tool_calls: [], tool_call_id: nil)
|
298
|
+
@llm_adapter.build_message(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
|
311
299
|
end
|
312
300
|
|
313
|
-
#
|
301
|
+
# Increment the tokens count based on the last interaction with the LLM
|
314
302
|
#
|
315
|
-
# @param
|
316
|
-
# @
|
317
|
-
|
318
|
-
|
303
|
+
# @param prompt_tokens [Integer] The number of used prmopt tokens
|
304
|
+
# @param completion_tokens [Integer] The number of used completion tokens
|
305
|
+
# @param total_tokens [Integer] The total number of used tokens
|
306
|
+
# @return [Integer] The current total tokens count
|
307
|
+
def record_used_tokens(prompt_tokens, completion_tokens, total_tokens_from_operation)
|
308
|
+
@total_prompt_tokens += prompt_tokens if prompt_tokens
|
309
|
+
@total_completion_tokens += completion_tokens if completion_tokens
|
310
|
+
@total_tokens += total_tokens_from_operation if total_tokens_from_operation
|
311
|
+
end
|
319
312
|
|
320
|
-
|
321
|
-
tool_name, method_name = function_name.split("__")
|
322
|
-
tool_arguments = tool_call.dig("input").transform_keys(&:to_sym)
|
313
|
+
# TODO: Fix the message truncation when context window is exceeded
|
323
314
|
|
324
|
-
|
325
|
-
|
315
|
+
module LLM
|
316
|
+
class Adapter
|
317
|
+
def self.build(llm)
|
318
|
+
case llm
|
319
|
+
when Langchain::LLM::Ollama
|
320
|
+
Adapters::Ollama.new
|
321
|
+
when Langchain::LLM::OpenAI
|
322
|
+
Adapters::OpenAI.new
|
323
|
+
when Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI
|
324
|
+
Adapters::GoogleGemini.new
|
325
|
+
when Langchain::LLM::Anthropic
|
326
|
+
Adapters::Anthropic.new
|
327
|
+
else
|
328
|
+
raise ArgumentError, "Unsupported LLM type: #{llm.class}"
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
326
332
|
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
tool_call_id = tool_call.dig("functionCall", "name")
|
333
|
+
module Adapters
|
334
|
+
class Base
|
335
|
+
def build_chat_params(tools:, instructions:, messages:)
|
336
|
+
raise NotImplementedError, "Subclasses must implement build_chat_params"
|
337
|
+
end
|
333
338
|
|
334
|
-
|
335
|
-
|
336
|
-
|
339
|
+
def extract_tool_call_args(tool_call:)
|
340
|
+
raise NotImplementedError, "Subclasses must implement extract_tool_call_args"
|
341
|
+
end
|
337
342
|
|
338
|
-
|
339
|
-
|
343
|
+
def build_message(role:, content: nil, tool_calls: [], tool_call_id: nil)
|
344
|
+
raise NotImplementedError, "Subclasses must implement build_message"
|
345
|
+
end
|
346
|
+
end
|
340
347
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
348
|
+
class Ollama < Base
|
349
|
+
def build_chat_params(tools:, instructions:, messages:)
|
350
|
+
params = {messages: messages}
|
351
|
+
if tools.any?
|
352
|
+
params[:tools] = tools.map { |tool| tool.class.function_schemas.to_openai_format }.flatten
|
353
|
+
end
|
354
|
+
params
|
355
|
+
end
|
356
|
+
|
357
|
+
def build_message(role:, content: nil, tool_calls: [], tool_call_id: nil)
|
358
|
+
Langchain::Messages::OllamaMessage.new(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
|
359
|
+
end
|
360
|
+
|
361
|
+
# Extract the tool call information from the OpenAI tool call hash
|
362
|
+
#
|
363
|
+
# @param tool_call [Hash] The tool call hash
|
364
|
+
# @return [Array] The tool call information
|
365
|
+
def extract_tool_call_args(tool_call:)
|
366
|
+
tool_call_id = tool_call.dig("id")
|
367
|
+
|
368
|
+
function_name = tool_call.dig("function", "name")
|
369
|
+
tool_name, method_name = function_name.split("__")
|
370
|
+
|
371
|
+
tool_arguments = tool_call.dig("function", "arguments")
|
372
|
+
tool_arguments = if tool_arguments.is_a?(Hash)
|
373
|
+
Langchain::Utils::HashTransformer.symbolize_keys(tool_arguments)
|
374
|
+
else
|
375
|
+
JSON.parse(tool_arguments, symbolize_names: true)
|
376
|
+
end
|
377
|
+
|
378
|
+
[tool_call_id, tool_name, method_name, tool_arguments]
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
class OpenAI < Base
|
383
|
+
def build_chat_params(tools:, instructions:, messages:)
|
384
|
+
params = {messages: messages}
|
385
|
+
if tools.any?
|
386
|
+
params[:tools] = tools.map { |tool| tool.class.function_schemas.to_openai_format }.flatten
|
387
|
+
params[:tool_choice] = "auto"
|
388
|
+
end
|
389
|
+
params
|
390
|
+
end
|
391
|
+
|
392
|
+
def build_message(role:, content: nil, tool_calls: [], tool_call_id: nil)
|
393
|
+
Langchain::Messages::OpenAIMessage.new(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
|
394
|
+
end
|
395
|
+
|
396
|
+
# Extract the tool call information from the OpenAI tool call hash
|
397
|
+
#
|
398
|
+
# @param tool_call [Hash] The tool call hash
|
399
|
+
# @return [Array] The tool call information
|
400
|
+
def extract_tool_call_args(tool_call:)
|
401
|
+
tool_call_id = tool_call.dig("id")
|
402
|
+
|
403
|
+
function_name = tool_call.dig("function", "name")
|
404
|
+
tool_name, method_name = function_name.split("__")
|
405
|
+
|
406
|
+
tool_arguments = tool_call.dig("function", "arguments")
|
407
|
+
tool_arguments = if tool_arguments.is_a?(Hash)
|
408
|
+
Langchain::Utils::HashTransformer.symbolize_keys(tool_arguments)
|
409
|
+
else
|
410
|
+
JSON.parse(tool_arguments, symbolize_names: true)
|
411
|
+
end
|
412
|
+
|
413
|
+
[tool_call_id, tool_name, method_name, tool_arguments]
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
class GoogleGemini < Base
|
418
|
+
def build_chat_params(tools:, instructions:, messages:)
|
419
|
+
params = {messages: messages}
|
420
|
+
if tools.any?
|
421
|
+
params[:tools] = tools.map { |tool| tool.class.function_schemas.to_google_gemini_format }.flatten
|
422
|
+
params[:system] = instructions if instructions
|
423
|
+
params[:tool_choice] = "auto"
|
424
|
+
end
|
425
|
+
params
|
426
|
+
end
|
427
|
+
|
428
|
+
def build_message(role:, content: nil, tool_calls: [], tool_call_id: nil)
|
429
|
+
Langchain::Messages::GoogleGeminiMessage.new(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
|
430
|
+
end
|
431
|
+
|
432
|
+
# Extract the tool call information from the Google Gemini tool call hash
|
433
|
+
#
|
434
|
+
# @param tool_call [Hash] The tool call hash, format: {"functionCall"=>{"name"=>"weather__execute", "args"=>{"input"=>"NYC"}}}
|
435
|
+
# @return [Array] The tool call information
|
436
|
+
def extract_tool_call_args(tool_call:)
|
437
|
+
tool_call_id = tool_call.dig("functionCall", "name")
|
438
|
+
function_name = tool_call.dig("functionCall", "name")
|
439
|
+
tool_name, method_name = function_name.split("__")
|
440
|
+
tool_arguments = tool_call.dig("functionCall", "args").transform_keys(&:to_sym)
|
441
|
+
[tool_call_id, tool_name, method_name, tool_arguments]
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
class Anthropic < Base
|
446
|
+
def build_chat_params(tools:, instructions:, messages:)
|
447
|
+
params = {messages: messages}
|
448
|
+
if tools.any?
|
449
|
+
params[:tools] = tools.map { |tool| tool.class.function_schemas.to_anthropic_format }.flatten
|
450
|
+
params[:tool_choice] = {type: "auto"}
|
451
|
+
end
|
452
|
+
params[:system] = instructions if instructions
|
453
|
+
params
|
454
|
+
end
|
455
|
+
|
456
|
+
def build_message(role:, content: nil, tool_calls: [], tool_call_id: nil)
|
457
|
+
Langchain::Messages::AnthropicMessage.new(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
|
458
|
+
end
|
459
|
+
|
460
|
+
# Extract the tool call information from the Anthropic tool call hash
|
461
|
+
#
|
462
|
+
# @param tool_call [Hash] The tool call hash, format: {"type"=>"tool_use", "id"=>"toolu_01TjusbFApEbwKPRWTRwzadR", "name"=>"news_retriever__get_top_headlines", "input"=>{"country"=>"us", "page_size"=>10}}], "stop_reason"=>"tool_use"}
|
463
|
+
# @return [Array] The tool call information
|
464
|
+
def extract_tool_call_args(tool_call:)
|
465
|
+
tool_call_id = tool_call.dig("id")
|
466
|
+
function_name = tool_call.dig("name")
|
467
|
+
tool_name, method_name = function_name.split("__")
|
468
|
+
tool_arguments = tool_call.dig("input").transform_keys(&:to_sym)
|
469
|
+
[tool_call_id, tool_name, method_name, tool_arguments]
|
470
|
+
end
|
471
|
+
end
|
355
472
|
end
|
356
473
|
end
|
357
|
-
|
358
|
-
# TODO: Fix the message truncation when context window is exceeded
|
359
474
|
end
|
360
475
|
end
|
@@ -0,0 +1,74 @@
|
|
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
|
+
# Convert the message to an OpenAI API-compatible hash
|
34
|
+
#
|
35
|
+
# @return [Hash] The message as an OpenAI API-compatible hash
|
36
|
+
def to_hash
|
37
|
+
{}.tap do |h|
|
38
|
+
h[:role] = role
|
39
|
+
h[:content] = content if content # Content is nil for tool calls
|
40
|
+
h[:tool_calls] = tool_calls if tool_calls.any?
|
41
|
+
h[:tool_call_id] = tool_call_id if tool_call_id
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Check if the message came from an LLM
|
46
|
+
#
|
47
|
+
# @return [Boolean] true/false whether this message was produced by an LLM
|
48
|
+
def llm?
|
49
|
+
assistant?
|
50
|
+
end
|
51
|
+
|
52
|
+
# Check if the message came from an LLM
|
53
|
+
#
|
54
|
+
# @return [Boolean] true/false whether this message was produced by an LLM
|
55
|
+
def assistant?
|
56
|
+
role == "assistant"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Check if the message are system instructions
|
60
|
+
#
|
61
|
+
# @return [Boolean] true/false whether this message are system instructions
|
62
|
+
def system?
|
63
|
+
role == "system"
|
64
|
+
end
|
65
|
+
|
66
|
+
# Check if the message is a tool call
|
67
|
+
#
|
68
|
+
# @return [Boolean] true/false whether this message is a tool call
|
69
|
+
def tool?
|
70
|
+
role == "tool"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
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
|
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
|
@@ -35,8 +35,8 @@ module Langchain
|
|
35
35
|
@logger.respond_to?(method, include_private)
|
36
36
|
end
|
37
37
|
|
38
|
-
def method_missing(method, *args, **kwargs, &)
|
39
|
-
return @logger.send(method, *args, **kwargs, &) unless @levels.include?(method)
|
38
|
+
def method_missing(method, *args, **kwargs, &block)
|
39
|
+
return @logger.send(method, *args, **kwargs, &block) unless @levels.include?(method)
|
40
40
|
|
41
41
|
for_class = kwargs.delete(:for)
|
42
42
|
for_class_name = for_class&.name
|
data/lib/langchain/llm/ai21.rb
CHANGED
@@ -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.
|
8
|
+
# gem "anthropic", "~> 0.3.0"
|
9
9
|
#
|
10
10
|
# Usage:
|
11
|
-
#
|
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 =
|
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!
|
data/lib/langchain/llm/azure.rb
CHANGED
@@ -42,17 +42,17 @@ module Langchain::LLM
|
|
42
42
|
|
43
43
|
def embed(...)
|
44
44
|
@client = @embed_client
|
45
|
-
super
|
45
|
+
super
|
46
46
|
end
|
47
47
|
|
48
48
|
def complete(...)
|
49
49
|
@client = @chat_client
|
50
|
-
super
|
50
|
+
super
|
51
51
|
end
|
52
52
|
|
53
53
|
def chat(...)
|
54
54
|
@client = @chat_client
|
55
|
-
super
|
55
|
+
super
|
56
56
|
end
|
57
57
|
end
|
58
58
|
end
|