langchainrb 0.14.0 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
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