langchainrb 0.14.0 → 0.15.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -2
  3. data/lib/langchain/assistants/assistant.rb +175 -131
  4. data/lib/langchain/assistants/messages/ollama_message.rb +9 -21
  5. data/lib/langchain/contextual_logger.rb +11 -5
  6. data/lib/langchain/evals/ragas/faithfulness.rb +5 -1
  7. data/lib/langchain/llm/google_gemini.rb +1 -1
  8. data/lib/langchain/llm/ollama.rb +23 -17
  9. data/lib/langchain/llm/openai.rb +1 -1
  10. data/lib/langchain/llm/response/ollama_response.rb +1 -15
  11. data/lib/langchain/llm/unified_parameters.rb +2 -2
  12. data/lib/langchain/tool/calculator.rb +38 -0
  13. data/lib/langchain/tool/{database/database.rb → database.rb} +24 -12
  14. data/lib/langchain/tool/file_system.rb +44 -0
  15. data/lib/langchain/tool/{google_search/google_search.rb → google_search.rb} +17 -23
  16. data/lib/langchain/tool/{news_retriever/news_retriever.rb → news_retriever.rb} +41 -14
  17. data/lib/langchain/tool/ruby_code_interpreter.rb +41 -0
  18. data/lib/langchain/tool/{tavily/tavily.rb → tavily.rb} +24 -10
  19. data/lib/langchain/tool/vectorsearch.rb +40 -0
  20. data/lib/langchain/tool/weather.rb +109 -0
  21. data/lib/langchain/tool/{wikipedia/wikipedia.rb → wikipedia.rb} +17 -13
  22. data/lib/langchain/tool_definition.rb +212 -0
  23. data/lib/langchain/utils/colorizer.rb +19 -0
  24. data/lib/langchain/utils/hash_transformer.rb +9 -17
  25. data/lib/langchain/utils/to_boolean.rb +27 -0
  26. data/lib/langchain/vectorsearch/chroma.rb +2 -2
  27. data/lib/langchain/vectorsearch/elasticsearch.rb +2 -2
  28. data/lib/langchain/vectorsearch/epsilla.rb +3 -3
  29. data/lib/langchain/vectorsearch/milvus.rb +2 -2
  30. data/lib/langchain/vectorsearch/pgvector.rb +2 -2
  31. data/lib/langchain/vectorsearch/pinecone.rb +2 -2
  32. data/lib/langchain/vectorsearch/qdrant.rb +2 -2
  33. data/lib/langchain/vectorsearch/weaviate.rb +4 -4
  34. data/lib/langchain/version.rb +1 -1
  35. data/lib/langchain.rb +1 -2
  36. metadata +18 -54
  37. data/lib/langchain/tool/base.rb +0 -107
  38. data/lib/langchain/tool/calculator/calculator.json +0 -19
  39. data/lib/langchain/tool/calculator/calculator.rb +0 -34
  40. data/lib/langchain/tool/database/database.json +0 -46
  41. data/lib/langchain/tool/file_system/file_system.json +0 -57
  42. data/lib/langchain/tool/file_system/file_system.rb +0 -32
  43. data/lib/langchain/tool/google_search/google_search.json +0 -19
  44. data/lib/langchain/tool/news_retriever/news_retriever.json +0 -122
  45. data/lib/langchain/tool/ruby_code_interpreter/ruby_code_interpreter.json +0 -19
  46. data/lib/langchain/tool/ruby_code_interpreter/ruby_code_interpreter.rb +0 -37
  47. data/lib/langchain/tool/tavily/tavily.json +0 -54
  48. data/lib/langchain/tool/vectorsearch/vectorsearch.json +0 -24
  49. data/lib/langchain/tool/vectorsearch/vectorsearch.rb +0 -36
  50. data/lib/langchain/tool/weather/weather.json +0 -19
  51. data/lib/langchain/tool/weather/weather.rb +0 -55
  52. data/lib/langchain/tool/wikipedia/wikipedia.json +0 -19
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68900cd116cf0fb1b77376a4906e5551f0d578ee2bb47c7ec86d32bf44f84e33
4
- data.tar.gz: f68782c3cdc856799778618d78b6411a85b0c69adf6a4d33489b8025fdca3dce
3
+ metadata.gz: a8a7ebf84076b609719daddc97e448f4d4fe987e8ee1eeaddb70df667f163d02
4
+ data.tar.gz: d52a9619b5913e8ce51563697e38841f9798e04f3caf294d7f331ff159eb70cb
5
5
  SHA512:
6
- metadata.gz: 158410fd769caaf9074eddc1143ddee9256ac5a466a510c32b74d337eba62fab80b676661cbf1673604d236014a5cb4defdd4743e71abb713a659ddea0fe5e8c
7
- data.tar.gz: 2e956356a443ff37ad711f6c42f8c4940925bcee4be075b403c78c3f702b487c12790dca9ba7d68a01acaf1c245b2910650b3f938e80cedd1fc2d5af14f7ffa8
6
+ metadata.gz: 2354d82608942a1744be426f23277b18fee508f539613327c9ed6f784d7da18f317d2266d356f3124eef93ea5052fe237f3fc639ff56db4b86060b08f16df1d2
7
+ data.tar.gz: 282873b85e832e9a872f007a5a6bf35400e22639c5c7f26ac535da4f06370f35d6de35a44d434ec81fedfcff1143a448440cf8bf76ae38e01d0ad5ec15f08dc7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.15.1] - 2024-08-19
4
+ - Drop `to_bool` gem in favour of custom util class
5
+ - Drop `colorize` which is GPL-licensed in favour of `rainbow`
6
+ - Improve Langchain::Tool::Weather tool
7
+
8
+ ## [0.15.0] - 2024-08-14
9
+ - Fix Langchain::Assistant when llm is Anthropic
10
+ - Fix GoogleGemini#chat method
11
+ - Langchain::LLM::Weaviate initializer does not require api_key anymore
12
+ - [BREAKING] Langchain::LLM::OpenAI#chat() uses `gpt-4o-mini` by default instead of `gpt-3.5-turbo` previously.
13
+ - [BREAKING] Assistant works with a number of open-source models via Ollama.
14
+ - [BREAKING] Introduce new `Langchain::ToolDefinition` module to define tools. This replaces the previous reliance on subclassing from `Langchain::Tool::Base`.
15
+
3
16
  ## [0.14.0] - 2024-07-12
4
17
  - Removed TokenLength validators
5
18
  - Assistant works with a Mistral LLM now
@@ -127,7 +140,7 @@
127
140
  - AWS Bedrock is available as an LLM provider. Available models from AI21, Cohere, AWS, and Anthropic.
128
141
 
129
142
  ## [0.7.3] - 2023-11-08
130
- - LLM response passes through the context in RAG cases
143
+ - LLM response passes through the context in RAG cases
131
144
  - Fix gpt-4 token length validation
132
145
 
133
146
  ## [0.7.2] - 2023-11-02
@@ -172,7 +185,7 @@
172
185
 
173
186
  ## [0.6.13] - 2023-08-23
174
187
  - Add `k:` parameter to all `ask()` vector search methods
175
- - Bump Faraday to 2.x
188
+ - Bump Faraday to 2.x
176
189
 
177
190
  ## [0.6.12] - 2023-08-13
178
191
 
@@ -19,14 +19,6 @@ module Langchain
19
19
  attr_reader :total_prompt_tokens, :total_completion_tokens, :total_tokens
20
20
  attr_accessor :tools
21
21
 
22
- SUPPORTED_LLMS = [
23
- Langchain::LLM::Anthropic,
24
- Langchain::LLM::GoogleGemini,
25
- Langchain::LLM::GoogleVertexAI,
26
- Langchain::LLM::Ollama,
27
- Langchain::LLM::OpenAI
28
- ]
29
-
30
22
  # Create a new assistant
31
23
  #
32
24
  # @param llm [Langchain::LLM::Base] LLM instance that the assistant will use
@@ -39,15 +31,12 @@ module Langchain
39
31
  tools: [],
40
32
  instructions: nil
41
33
  )
42
- unless SUPPORTED_LLMS.include?(llm.class)
43
- raise ArgumentError, "Invalid LLM; currently only #{SUPPORTED_LLMS.join(", ")} are supported"
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"
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"
47
36
  end
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) }
49
37
 
50
38
  @llm = llm
39
+ @llm_adapter = LLM::Adapter.build(llm)
51
40
  @thread = thread || Langchain::Thread.new
52
41
  @tools = tools
53
42
  @instructions = instructions
@@ -214,14 +203,7 @@ module Langchain
214
203
  def handle_user_or_tool_message
215
204
  response = chat_with_llm
216
205
 
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)
206
+ add_message(role: response.role, content: response.chat_completion, tool_calls: response.tool_calls)
225
207
  record_used_tokens(response.prompt_tokens, response.completion_tokens, response.total_tokens)
226
208
 
227
209
  set_state_for(response: response)
@@ -247,7 +229,7 @@ module Langchain
247
229
  run_tools(thread.messages.last.tool_calls)
248
230
  :in_progress
249
231
  rescue => e
250
- Langchain.logger.error("Error running tools: #{e.message}")
232
+ Langchain.logger.error("Error running tools: #{e.message}; #{e.backtrace.join('\n')}")
251
233
  :failed
252
234
  end
253
235
 
@@ -268,17 +250,7 @@ module Langchain
268
250
  end
269
251
 
270
252
  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)
253
+ if llm.is_a?(Langchain::LLM::OpenAI)
282
254
  add_message(role: "system", content: instructions) if instructions
283
255
  end
284
256
  end
@@ -289,36 +261,12 @@ module Langchain
289
261
  def chat_with_llm
290
262
  Langchain.logger.info("Sending a call to #{llm.class}", for: self.class)
291
263
 
292
- params = {}
293
-
294
- if llm.is_a?(Langchain::LLM::OpenAI)
295
- if tools.any?
296
- params[:tools] = tools.map(&:to_openai_tools).flatten
297
- params[:tool_choice] = "auto"
298
- end
299
- elsif llm.is_a?(Langchain::LLM::Anthropic)
300
- if tools.any?
301
- params[:tools] = tools.map(&:to_anthropic_tools).flatten
302
- params[:tool_choice] = {type: "auto"}
303
- end
304
- params[:system] = instructions if instructions
305
- elsif [Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI].include?(llm.class)
306
- if tools.any?
307
- params[:tools] = tools.map(&:to_google_gemini_tools).flatten
308
- params[:system] = instructions if instructions
309
- params[:tool_choice] = "auto"
310
- end
311
- end
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
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)
322
270
  end
323
271
 
324
272
  # Run the tools automatically
@@ -327,18 +275,10 @@ module Langchain
327
275
  def run_tools(tool_calls)
328
276
  # Iterate over each function invocation and submit tool output
329
277
  tool_calls.each do |tool_call|
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)
333
- extract_openai_tool_call(tool_call: tool_call)
334
- elsif [Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI].include?(llm.class)
335
- extract_google_gemini_tool_call(tool_call: tool_call)
336
- elsif llm.is_a?(Langchain::LLM::Anthropic)
337
- extract_anthropic_tool_call(tool_call: tool_call)
338
- end
278
+ tool_call_id, tool_name, method_name, tool_arguments = @llm_adapter.extract_tool_call_args(tool_call: tool_call)
339
279
 
340
280
  tool_instance = tools.find do |t|
341
- t.name == tool_name
281
+ t.class.tool_name == tool_name
342
282
  end or raise ArgumentError, "Tool not found in assistant.tools"
343
283
 
344
284
  output = tool_instance.send(method_name, **tool_arguments)
@@ -347,54 +287,6 @@ module Langchain
347
287
  end
348
288
  end
349
289
 
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]
354
- end
355
-
356
- # Extract the tool call information from the OpenAI tool call hash
357
- #
358
- # @param tool_call [Hash] The tool call hash
359
- # @return [Array] The tool call information
360
- def extract_openai_tool_call(tool_call:)
361
- tool_call_id = tool_call.dig("id")
362
-
363
- function_name = tool_call.dig("function", "name")
364
- tool_name, method_name = function_name.split("__")
365
- tool_arguments = JSON.parse(tool_call.dig("function", "arguments"), symbolize_names: true)
366
-
367
- [tool_call_id, tool_name, method_name, tool_arguments]
368
- end
369
-
370
- # Extract the tool call information from the Anthropic tool call hash
371
- #
372
- # @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"}
373
- # @return [Array] The tool call information
374
- def extract_anthropic_tool_call(tool_call:)
375
- tool_call_id = tool_call.dig("id")
376
-
377
- function_name = tool_call.dig("name")
378
- tool_name, method_name = function_name.split("__")
379
- tool_arguments = tool_call.dig("input").transform_keys(&:to_sym)
380
-
381
- [tool_call_id, tool_name, method_name, tool_arguments]
382
- end
383
-
384
- # Extract the tool call information from the Google Gemini tool call hash
385
- #
386
- # @param tool_call [Hash] The tool call hash, format: {"functionCall"=>{"name"=>"weather__execute", "args"=>{"input"=>"NYC"}}}
387
- # @return [Array] The tool call information
388
- def extract_google_gemini_tool_call(tool_call:)
389
- tool_call_id = tool_call.dig("functionCall", "name")
390
-
391
- function_name = tool_call.dig("functionCall", "name")
392
- tool_name, method_name = function_name.split("__")
393
- tool_arguments = tool_call.dig("functionCall", "args").transform_keys(&:to_sym)
394
-
395
- [tool_call_id, tool_name, method_name, tool_arguments]
396
- end
397
-
398
290
  # Build a message
399
291
  #
400
292
  # @param role [String] The role of the message
@@ -403,15 +295,7 @@ module Langchain
403
295
  # @param tool_call_id [String] The ID of the tool call to include in the message
404
296
  # @return [Langchain::Message] The Message object
405
297
  def build_message(role:, content: nil, tool_calls: [], tool_call_id: nil)
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)
409
- Langchain::Messages::OpenAIMessage.new(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
410
- elsif [Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI].include?(llm.class)
411
- Langchain::Messages::GoogleGeminiMessage.new(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
412
- elsif llm.is_a?(Langchain::LLM::Anthropic)
413
- Langchain::Messages::AnthropicMessage.new(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
414
- end
298
+ @llm_adapter.build_message(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
415
299
  end
416
300
 
417
301
  # Increment the tokens count based on the last interaction with the LLM
@@ -427,5 +311,165 @@ module Langchain
427
311
  end
428
312
 
429
313
  # TODO: Fix the message truncation when context window is exceeded
314
+
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
332
+
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
338
+
339
+ def extract_tool_call_args(tool_call:)
340
+ raise NotImplementedError, "Subclasses must implement extract_tool_call_args"
341
+ end
342
+
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
347
+
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
472
+ end
473
+ end
430
474
  end
431
475
  end
@@ -30,27 +30,15 @@ module Langchain
30
30
  @tool_call_id = tool_call_id
31
31
  end
32
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
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
54
42
  end
55
43
  end
56
44
 
@@ -35,20 +35,20 @@ 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
43
43
 
44
44
  log_line_parts = []
45
- log_line_parts << "[Langchain.rb]".colorize(color: :yellow)
45
+ log_line_parts << colorize("[Langchain.rb]", color: :yellow)
46
46
  log_line_parts << if for_class.respond_to?(:logger_options)
47
- "[#{for_class_name}]".colorize(for_class.logger_options) + ":"
47
+ colorize("[#{for_class_name}]", for_class.logger_options) + ":"
48
48
  elsif for_class_name
49
49
  "[#{for_class_name}]:"
50
50
  end
51
- log_line_parts << args.first.colorize(MESSAGE_COLOR_OPTIONS[method])
51
+ log_line_parts << colorize(args.first, MESSAGE_COLOR_OPTIONS[method])
52
52
  log_line = log_line_parts.compact.join(" ")
53
53
 
54
54
  @logger.send(
@@ -56,5 +56,11 @@ module Langchain
56
56
  log_line
57
57
  )
58
58
  end
59
+
60
+ private
61
+
62
+ def colorize(line, options)
63
+ Langchain::Utils::Colorizer.colorize(line, options)
64
+ end
59
65
  end
60
66
  end
@@ -47,7 +47,7 @@ module Langchain
47
47
  verdicts = match.captures.first
48
48
  verdicts
49
49
  .split(".")
50
- .count { |value| value.strip.to_boolean }
50
+ .count { |value| to_boolean(value.strip) }
51
51
  end
52
52
 
53
53
  def statements_verification(statements:, context:)
@@ -79,6 +79,10 @@ module Langchain
79
79
  file_path: Langchain.root.join("langchain/evals/ragas/prompts/faithfulness_statements_extraction.yml")
80
80
  )
81
81
  end
82
+
83
+ def to_boolean(value)
84
+ Langchain::Utils::ToBoolean.new.to_bool(value)
85
+ end
82
86
  end
83
87
  end
84
88
  end
@@ -62,7 +62,7 @@ module Langchain::LLM
62
62
 
63
63
  request = Net::HTTP::Post.new(uri)
64
64
  request.content_type = "application/json"
65
- request.body = Langchain::Utils::HashTransformer.deep_transform_keys(parameters) { |key| Langchain::Utils::HashTransformer.camelize_lower(key.to_s).to_sym }.to_json
65
+ request.body = parameters.to_json
66
66
 
67
67
  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
68
68
  http.request(request)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/hash"
4
-
5
3
  module Langchain::LLM
6
4
  # Interface to Ollama API.
7
5
  # Available models: https://ollama.ai/library
@@ -15,9 +13,9 @@ module Langchain::LLM
15
13
 
16
14
  DEFAULTS = {
17
15
  temperature: 0.0,
18
- completion_model_name: "llama3",
19
- embeddings_model_name: "llama3",
20
- chat_completion_model_name: "llama3"
16
+ completion_model_name: "llama3.1",
17
+ embeddings_model_name: "llama3.1",
18
+ chat_completion_model_name: "llama3.1"
21
19
  }.freeze
22
20
 
23
21
  EMBEDDING_SIZES = {
@@ -25,20 +23,24 @@ module Langchain::LLM
25
23
  "dolphin-mixtral": 4_096,
26
24
  llama2: 4_096,
27
25
  llama3: 4_096,
26
+ "llama3.1": 4_096,
28
27
  llava: 4_096,
29
28
  mistral: 4_096,
30
29
  "mistral-openorca": 4_096,
31
- mixtral: 4_096
30
+ mixtral: 4_096,
31
+ tinydolphin: 2_048
32
32
  }.freeze
33
33
 
34
34
  # Initialize the Ollama client
35
35
  # @param url [String] The URL of the Ollama instance
36
+ # @param api_key [String] The API key to use. This is optional and used when you expose Ollama API using Open WebUI
36
37
  # @param default_options [Hash] The default options to use
37
38
  #
38
- def initialize(url: "http://localhost:11434", default_options: {})
39
+ def initialize(url: "http://localhost:11434", api_key: nil, default_options: {})
39
40
  depends_on "faraday"
40
41
  @url = url
41
- @defaults = DEFAULTS.deep_merge(default_options)
42
+ @api_key = api_key
43
+ @defaults = DEFAULTS.merge(default_options)
42
44
  chat_parameters.update(
43
45
  model: {default: @defaults[:chat_completion_model_name]},
44
46
  temperature: {default: @defaults[:temperature]},
@@ -113,7 +115,7 @@ module Langchain::LLM
113
115
  system: system,
114
116
  template: template,
115
117
  context: context,
116
- stream: block.present?,
118
+ stream: block_given?, # rubocop:disable Performance/BlockGivenWithExplicitBlock
117
119
  raw: raw
118
120
  }.compact
119
121
 
@@ -173,7 +175,7 @@ module Langchain::LLM
173
175
  # content: the content of the message
174
176
  # images (optional): a list of images to include in the message (for multimodal models such as llava)
175
177
  def chat(messages:, model: nil, **params, &block)
176
- parameters = chat_parameters.to_params(params.merge(messages:, model:, stream: block.present?))
178
+ parameters = chat_parameters.to_params(params.merge(messages:, model:, stream: block_given?)) # rubocop:disable Performance/BlockGivenWithExplicitBlock
177
179
  responses_stream = []
178
180
 
179
181
  client.post("api/chat", parameters) do |req|
@@ -264,13 +266,20 @@ module Langchain::LLM
264
266
  private
265
267
 
266
268
  def client
267
- @client ||= Faraday.new(url: url) do |conn|
269
+ @client ||= Faraday.new(url: url, headers: auth_headers) do |conn|
268
270
  conn.request :json
269
271
  conn.response :json
270
272
  conn.response :raise_error
273
+ conn.response :logger, nil, {headers: true, bodies: true, errors: true}
271
274
  end
272
275
  end
273
276
 
277
+ def auth_headers
278
+ return unless @api_key
279
+
280
+ {"Authorization" => "Bearer #{@api_key}"}
281
+ end
282
+
274
283
  def json_responses_chunk_handler(&block)
275
284
  proc do |chunk, _size|
276
285
  chunk.split("\n").each do |chunk_line|
@@ -288,13 +297,10 @@ module Langchain::LLM
288
297
  OllamaResponse.new(final_response, model: parameters[:model])
289
298
  end
290
299
 
300
+ # BUG: If streamed, this method does not currently return the tool_calls response.
291
301
  def generate_final_chat_completion_response(responses_stream, parameters)
292
- final_response = responses_stream.last.merge(
293
- "message" => {
294
- "role" => "assistant",
295
- "content" => responses_stream.map { |resp| resp.dig("message", "content") }.join
296
- }
297
- )
302
+ final_response = responses_stream.last
303
+ final_response["message"]["content"] = responses_stream.map { |resp| resp.dig("message", "content") }.join
298
304
 
299
305
  OllamaResponse.new(final_response, model: parameters[:model])
300
306
  end
@@ -16,7 +16,7 @@ module Langchain::LLM
16
16
  DEFAULTS = {
17
17
  n: 1,
18
18
  temperature: 0.0,
19
- chat_completion_model_name: "gpt-3.5-turbo",
19
+ chat_completion_model_name: "gpt-4o-mini",
20
20
  embeddings_model_name: "text-embedding-3-small"
21
21
  }.freeze
22
22
 
@@ -48,21 +48,7 @@ module Langchain::LLM
48
48
  end
49
49
 
50
50
  def tool_calls
51
- if chat_completion && (parsed_tool_calls = JSON.parse(chat_completion))
52
- [parsed_tool_calls]
53
- elsif completion&.include?("[TOOL_CALLS]") && (
54
- parsed_tool_calls = JSON.parse(
55
- completion
56
- # Slice out the serialize JSON
57
- .slice(/\{.*\}/)
58
- # Replace hash rocket with colon
59
- .gsub("=>", ":")
60
- )
61
- )
62
- [parsed_tool_calls]
63
- else
64
- []
65
- end
51
+ Array(raw_response.dig("message", "tool_calls"))
66
52
  end
67
53
 
68
54
  private