langchainrb 0.14.0 → 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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  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 +2 -2
  6. data/lib/langchain/llm/google_gemini.rb +1 -1
  7. data/lib/langchain/llm/ollama.rb +23 -17
  8. data/lib/langchain/llm/openai.rb +1 -1
  9. data/lib/langchain/llm/response/ollama_response.rb +1 -15
  10. data/lib/langchain/llm/unified_parameters.rb +2 -2
  11. data/lib/langchain/tool/calculator.rb +38 -0
  12. data/lib/langchain/tool/{database/database.rb → database.rb} +24 -12
  13. data/lib/langchain/tool/file_system.rb +44 -0
  14. data/lib/langchain/tool/{google_search/google_search.rb → google_search.rb} +17 -23
  15. data/lib/langchain/tool/{news_retriever/news_retriever.rb → news_retriever.rb} +41 -14
  16. data/lib/langchain/tool/ruby_code_interpreter.rb +41 -0
  17. data/lib/langchain/tool/{tavily/tavily.rb → tavily.rb} +24 -10
  18. data/lib/langchain/tool/vectorsearch.rb +40 -0
  19. data/lib/langchain/tool/{weather/weather.rb → weather.rb} +21 -17
  20. data/lib/langchain/tool/{wikipedia/wikipedia.rb → wikipedia.rb} +17 -13
  21. data/lib/langchain/tool_definition.rb +212 -0
  22. data/lib/langchain/utils/hash_transformer.rb +9 -17
  23. data/lib/langchain/vectorsearch/chroma.rb +2 -2
  24. data/lib/langchain/vectorsearch/elasticsearch.rb +2 -2
  25. data/lib/langchain/vectorsearch/epsilla.rb +3 -3
  26. data/lib/langchain/vectorsearch/milvus.rb +2 -2
  27. data/lib/langchain/vectorsearch/pgvector.rb +2 -2
  28. data/lib/langchain/vectorsearch/pinecone.rb +2 -2
  29. data/lib/langchain/vectorsearch/qdrant.rb +2 -2
  30. data/lib/langchain/vectorsearch/weaviate.rb +4 -4
  31. data/lib/langchain/version.rb +1 -1
  32. metadata +13 -23
  33. data/lib/langchain/tool/base.rb +0 -107
  34. data/lib/langchain/tool/calculator/calculator.json +0 -19
  35. data/lib/langchain/tool/calculator/calculator.rb +0 -34
  36. data/lib/langchain/tool/database/database.json +0 -46
  37. data/lib/langchain/tool/file_system/file_system.json +0 -57
  38. data/lib/langchain/tool/file_system/file_system.rb +0 -32
  39. data/lib/langchain/tool/google_search/google_search.json +0 -19
  40. data/lib/langchain/tool/news_retriever/news_retriever.json +0 -122
  41. data/lib/langchain/tool/ruby_code_interpreter/ruby_code_interpreter.json +0 -19
  42. data/lib/langchain/tool/ruby_code_interpreter/ruby_code_interpreter.rb +0 -37
  43. data/lib/langchain/tool/tavily/tavily.json +0 -54
  44. data/lib/langchain/tool/vectorsearch/vectorsearch.json +0 -24
  45. data/lib/langchain/tool/vectorsearch/vectorsearch.rb +0 -36
  46. data/lib/langchain/tool/weather/weather.json +0 -19
  47. 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: dde504e05b1cbb32c857569bf71301537fed2deb468f1bdd69a7ef900a41c085
4
+ data.tar.gz: '08659cddd6f0bb285e167c7a35dbd2f83c2e9bb51a69206217ea91649e99839c'
5
5
  SHA512:
6
- metadata.gz: 158410fd769caaf9074eddc1143ddee9256ac5a466a510c32b74d337eba62fab80b676661cbf1673604d236014a5cb4defdd4743e71abb713a659ddea0fe5e8c
7
- data.tar.gz: 2e956356a443ff37ad711f6c42f8c4940925bcee4be075b403c78c3f702b487c12790dca9ba7d68a01acaf1c245b2910650b3f938e80cedd1fc2d5af14f7ffa8
6
+ metadata.gz: ce4dd091498659a2d8dda4b54e9e9584dc19be5f390dc5f1d98efa054a264134dc3510f2f83c65bdf23edfbd7344587b91113e69c2ea1fea2cdc157317735799
7
+ data.tar.gz: a6df110aa7d96c87402164f67aadab0a97e2a62b68b7466cf630fe79dd0611a1740ae11163361eef9c98fc816f7ba12d7bfc0aa2225759cc8191f59fead8fcbd
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
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
+
3
11
  ## [0.14.0] - 2024-07-12
4
12
  - Removed TokenLength validators
5
13
  - Assistant works with a Mistral LLM now
@@ -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,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
@@ -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
@@ -77,8 +77,8 @@ module Langchain::LLM
77
77
  @parameters.to_h
78
78
  end
79
79
 
80
- def each(&)
81
- to_params.each(&)
80
+ def each(&block)
81
+ to_params.each(&block)
82
82
  end
83
83
 
84
84
  def <=>(other)
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langchain::Tool
4
+ #
5
+ # A calculator tool that falls back to the Google calculator widget
6
+ #
7
+ # Gem requirements:
8
+ # gem "eqn", "~> 1.6.5"
9
+ # gem "google_search_results", "~> 2.0.0"
10
+ #
11
+ # Usage:
12
+ # calculator = Langchain::Tool::Calculator.new
13
+ #
14
+ class Calculator
15
+ extend Langchain::ToolDefinition
16
+ include Langchain::DependencyHelper
17
+
18
+ define_function :execute, description: "Evaluates a pure math expression or if equation contains non-math characters (e.g.: \"12F in Celsius\") then it uses the google search calculator to evaluate the expression" do
19
+ property :input, type: "string", description: "Math expression", required: true
20
+ end
21
+
22
+ def initialize
23
+ depends_on "eqn"
24
+ end
25
+
26
+ # Evaluates a pure math expression or if equation contains non-math characters (e.g.: "12F in Celsius") then it uses the google search calculator to evaluate the expression
27
+ #
28
+ # @param input [String] math expression
29
+ # @return [String] Answer
30
+ def execute(input:)
31
+ Langchain.logger.info("Executing \"#{input}\"", for: self.class)
32
+
33
+ Eqn::Calculator.calc(input)
34
+ rescue Eqn::ParseError, Eqn::NoVariableValueError
35
+ "\"#{input}\" is an invalid mathematical expression"
36
+ end
37
+ end
38
+ end