langchainrb 0.15.3 → 0.15.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c8d4d8e7dd770a724268c97b24f40ba812c1ec2b60e37242ee97da9c83a6c7d
4
- data.tar.gz: 8b98e726a7079ef750324052ad6681e81f92327ba74d82ea2445ee1212e048f6
3
+ metadata.gz: cb478a5261da82b78d6a90b6179e4050ae0fcba274c370b507f546b59cfd1a78
4
+ data.tar.gz: 7fb27d8b3c68060e923e69b1c67680be20846e5a384def4c6ce0efd1c9fa56ab
5
5
  SHA512:
6
- metadata.gz: d5189bbf3830a802aecf9ac67e4723b49bcbd68af5fbba6c107b656eb3658318f9a664a70344d77ae67972c1277bf19f606bf46c338d1d3f6ac4461c2f135d6b
7
- data.tar.gz: b6afcec28e4228a402dfee5d8f0aefd17a2d6bcbbacece31a6781d4e4c39433d16699c92aad7147a38553a61f63154335f39cbe8240fd5b4b618f25e97d0fb5f
6
+ metadata.gz: b6533a4064e822c78cc7659c55c948b13b4904d75ab99fb50269f82d7f815e02ff3c0659f0b6c1905b16c0834f2577ccd136cb3d1a34db6e26b7352c77350a10
7
+ data.tar.gz: 170e767e0fdef21fc6051a161904ee867205bb18a98641b545adf7fe3933b499d157be52009fd20e168efa5d94e5a03c39a1a21c94c2747b8e6a0393f2099e4e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.15.4] - 2024-09-10 🇧🇦
4
+ - Fix for Langchain::Prompt::PromptTemplate supporting nested JSON data
5
+ - Require common libs at top-level
6
+ - Add `add_message_callback` to `Langchain::Assistant` constructor to invoke an optional function when any message is added to the conversation
7
+ - Adding Assistant syntactic sugar with #run! and #add_message_and_run!
8
+
9
+ ## [0.15.4] - 2024-08-30
10
+ - Improve the Langchain::Tool::Database tool
11
+ - Allow explictly setting tool_choice on the Assistant instance
12
+ - Add support for bulk embedding in Ollama
13
+ - `Langchain::Assistant` works with `Langchain::LLM::MistralAI` llm
14
+ - Fix Langchain::LLM::Azure not applying full default_options
15
+
3
16
  ## [0.15.3] - 2024-08-27
4
17
  - Fix OpenAI#embed when text-embedding-ada-002 is used
5
18
 
data/README.md CHANGED
@@ -402,75 +402,108 @@ client.ask(question: "...")
402
402
  ```
403
403
 
404
404
  ## Assistants
405
- Assistants are Agent-like objects that leverage helpful instructions, LLMs, tools and knowledge to respond to user queries. Assistants can be configured with an LLM of your choice (currently only OpenAI), any vector search database and easily extended with additional tools.
406
-
407
- ### Available Tools 🛠️
408
-
409
- | Name | Description | ENV Requirements | Gem Requirements |
410
- | ------------ | :------------------------------------------------: | :-----------------------------------------------------------: | :---------------------------------------: |
411
- | "calculator" | Useful for getting the result of a math expression | | `gem "eqn", "~> 1.6.5"` |
412
- | "database" | Useful for querying a SQL database | | `gem "sequel", "~> 5.68.0"` |
413
- | "file_system" | Interacts with the file system | | |
414
- | "ruby_code_interpreter" | Interprets Ruby expressions | | `gem "safe_ruby", "~> 1.0.4"` |
415
- | "google_search" | A wrapper around Google Search | `ENV["SERPAPI_API_KEY"]` (https://serpapi.com/manage-api-key) | `gem "google_search_results", "~> 2.0.0"` |
416
- | "news_retriever" | A wrapper around NewsApi.org | `ENV["NEWS_API_KEY"]` (https://newsapi.org/) | |
417
- | "tavily" | A wrapper around Tavily AI | `ENV["TAVILY_API_KEY"]` (https://tavily.com/) | |
418
- | "weather" | Calls Open Weather API to retrieve the current weather | `ENV["OPEN_WEATHER_API_KEY"]` (https://home.openweathermap.org/api_keys) | |
419
- | "wikipedia" | Calls Wikipedia API to retrieve the summary | | `gem "wikipedia-client", "~> 1.17.0"` |
405
+ `Langchain::Assistant` is a powerful and flexible class that combines Large Language Models (LLMs), tools, and conversation management to create intelligent, interactive assistants. It's designed to handle complex conversations, execute tools, and provide coherent responses based on the context of the interaction.
420
406
 
421
- ### Demos
422
- 1. [Building an AI Assistant that operates a simulated E-commerce Store](https://www.loom.com/share/83aa4fd8dccb492aad4ca95da40ed0b2)
423
- 2. [New Langchain.rb Assistants interface](https://www.loom.com/share/e883a4a49b8746c1b0acf9d58cf6da36)
424
- 3. [Langchain.rb Assistant demo with NewsRetriever and function calling on Gemini](https://youtu.be/-ieyahrpDpM&t=1477s) - [code](https://github.com/palladius/gemini-news-crawler)
407
+ ### Features
408
+ * Supports multiple LLM providers (OpenAI, Google Gemini, Anthropic, Mistral AI and open-source models via Ollama)
409
+ * Integrates with various tools to extend functionality
410
+ * Manages conversation threads
411
+ * Handles automatic and manual tool execution
412
+ * Supports different message formats for various LLM providers
425
413
 
426
- ### Creating an Assistant
427
- 1. Instantiate an LLM of your choice
414
+ ### Usage
428
415
  ```ruby
429
416
  llm = Langchain::LLM::OpenAI.new(api_key: ENV["OPENAI_API_KEY"])
430
- ```
431
- 2. Instantiate an Assistant
432
- ```ruby
433
417
  assistant = Langchain::Assistant.new(
434
418
  llm: llm,
435
- instructions: "You are a Meteorologist Assistant that is able to pull the weather for any location",
436
- tools: [
437
- Langchain::Tool::Weather.new(api_key: ENV["OPEN_WEATHER_API_KEY"])
438
- ]
419
+ instructions: "You're a helpful AI assistant",
420
+ tools: [Langchain::Tool::NewsRetriever.new(api_key: ENV["NEWS_API_KEY"])]
439
421
  )
440
- ```
441
- ### Using an Assistant
442
- You can now add your message to an Assistant.
443
- ```ruby
444
- assistant.add_message content: "What's the weather in New York, New York?"
445
- ```
446
422
 
447
- Run the Assistant to generate a response.
448
- ```ruby
449
- assistant.run
423
+ # Add a user message and run the assistant
424
+ assistant.add_message_and_run(content: "What's the latest news about AI?")
425
+
426
+ # Access the conversation thread
427
+ messages = assistant.messages
428
+
429
+ # Run the assistant with automatic tool execution
430
+ assistant.run!
431
+ ```
432
+
433
+ ### Configuration
434
+ * `llm`: The LLM instance to use (required)
435
+ * `tools`: An array of tool instances (optional)
436
+ * `instructions`: System instructions for the assistant (optional)
437
+ * `tool_choice`: Specifies how tools should be selected. Default: "auto". A specific tool function name can be passed. This will force the Assistant to **always** use this function.
438
+ * `add_message_callback`: A callback function (proc, lambda) that is called when any message is added to the conversation (optional)
439
+
440
+ ### Key Methods
441
+ * `add_message`: Adds a user message to the messages array
442
+ * `run!`: Processes the conversation and generates responses
443
+ * `add_message_and_run!`: Combines adding a message and running the assistant
444
+ * `submit_tool_output`: Manually submit output to a tool call
445
+ * `messages`: Returns a list of ongoing messages
446
+
447
+ ### Built-in Tools 🛠️
448
+ * `Langchain::Tool::Calculator`: Useful for evaluating math expressions. Requires `gem "eqn"`.
449
+ * `Langchain::Tool::Database`: Connect your SQL database. Requires `gem "sequel"`.
450
+ * `Langchain::Tool::FileSystem`: Interact with the file system (read & write).
451
+ * `Langchain::Tool::RubyCodeInterpreter`: Useful for evaluating generated Ruby code. Requires `gem "safe_ruby"` (In need of a better solution).
452
+ * `Langchain::Tool::NewsRetriever`: A wrapper around [NewsApi.org](https://newsapi.org) to fetch news articles.
453
+ * `Langchain::Tool::Tavily`: A wrapper around [Tavily AI](https://tavily.com).
454
+ * `Langchain::Tool::Weather`: Calls [Open Weather API](https://home.openweathermap.org) to retrieve the current weather.
455
+ * `Langchain::Tool::Wikipedia`: Calls Wikipedia API.
456
+
457
+ ### Creating custom Tools
458
+ The Langchain::Assistant can be easily extended with custom tools by creating classes that `extend Langchain::ToolDefinition` module and implement required methods.
459
+ ```ruby
460
+ class MovieInfoTool
461
+ include Langchain::ToolDefinition
462
+
463
+ define_function :search_movie, description: "MovieInfoTool: Search for a movie by title" do
464
+ property :query, type: "string", description: "The movie title to search for", required: true
465
+ end
466
+
467
+ define_function :get_movie_details, description: "MovieInfoTool: Get detailed information about a specific movie" do
468
+ property :movie_id, type: "integer", description: "The TMDb ID of the movie", required: true
469
+ end
470
+
471
+ def initialize(api_key:)
472
+ @api_key = api_key
473
+ end
474
+
475
+ def search_movie(query:)
476
+ ...
477
+ end
478
+
479
+ def get_movie_details(movie_id:)
480
+ ...
481
+ end
482
+ end
450
483
  ```
451
484
 
452
- If a Tool is invoked you can manually submit an output.
485
+ #### Example usage:
453
486
  ```ruby
454
- assistant.submit_tool_output tool_call_id: "...", output: "It's 70 degrees and sunny in New York City"
455
- ```
487
+ movie_tool = MovieInfoTool.new(api_key: "...")
456
488
 
457
- Or run the assistant with `auto_tool_execution: tool` to call Tools automatically.
458
- ```ruby
459
- assistant.add_message content: "How about San Diego, CA?"
460
- assistant.run(auto_tool_execution: true)
461
- ```
462
- You can also combine the two by calling:
463
- ```ruby
464
- assistant.add_message_and_run content: "What about Sacramento, CA?", auto_tool_execution: true
465
- ```
489
+ assistant = Langchain::Assistant.new(
490
+ llm: llm,
491
+ instructions: "You're a helpful AI assistant that can provide movie information",
492
+ tools: [movie_tool]
493
+ )
466
494
 
467
- ### Accessing Thread messages
468
- You can access the messages in a Thread by calling `assistant.thread.messages`.
469
- ```ruby
470
- assistant.messages
495
+ assistant.add_message_and_run(content: "Can you tell me about the movie 'Inception'?")
496
+ # Check the response in the last message in the conversation
497
+ assistant.messages.last
471
498
  ```
472
499
 
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.
500
+ ### Error Handling
501
+ The assistant includes error handling for invalid inputs, unsupported LLM types, and tool execution failures. It uses a state machine to manage the conversation flow and handle different scenarios gracefully.
502
+
503
+ ### Demos
504
+ 1. [Building an AI Assistant that operates a simulated E-commerce Store](https://www.loom.com/share/83aa4fd8dccb492aad4ca95da40ed0b2)
505
+ 2. [New Langchain.rb Assistants interface](https://www.loom.com/share/e883a4a49b8746c1b0acf9d58cf6da36)
506
+ 3. [Langchain.rb Assistant demo with NewsRetriever and function calling on Gemini](https://youtu.be/-ieyahrpDpM&t=1477s) - [code](https://github.com/palladius/gemini-news-crawler)
474
507
 
475
508
  ## Evaluations (Evals)
476
509
  The Evaluations module is a collection of tools that can be used to evaluate and track the performance of the output products by LLM and your RAG (Retrieval Augmented Generation) pipelines.
@@ -15,7 +15,7 @@ module Langchain
15
15
  extend Forwardable
16
16
  def_delegators :thread, :messages
17
17
 
18
- attr_reader :llm, :thread, :instructions, :state
18
+ attr_reader :llm, :thread, :instructions, :state, :llm_adapter, :tool_choice
19
19
  attr_reader :total_prompt_tokens, :total_completion_tokens, :total_tokens
20
20
  attr_accessor :tools
21
21
 
@@ -29,7 +29,9 @@ module Langchain
29
29
  llm:,
30
30
  thread: nil,
31
31
  tools: [],
32
- instructions: nil
32
+ instructions: nil,
33
+ tool_choice: "auto",
34
+ add_message_callback: nil
33
35
  )
34
36
  unless tools.is_a?(Array) && tools.all? { |tool| tool.class.singleton_class.included_modules.include?(Langchain::ToolDefinition) }
35
37
  raise ArgumentError, "Tools must be an array of objects extending Langchain::ToolDefinition"
@@ -37,8 +39,12 @@ module Langchain
37
39
 
38
40
  @llm = llm
39
41
  @llm_adapter = LLM::Adapter.build(llm)
42
+
40
43
  @thread = thread || Langchain::Thread.new
44
+ @thread.add_message_callback = add_message_callback
45
+
41
46
  @tools = tools
47
+ self.tool_choice = tool_choice
42
48
  @instructions = instructions
43
49
  @state = :ready
44
50
 
@@ -49,9 +55,8 @@ module Langchain
49
55
  raise ArgumentError, "Thread must be an instance of Langchain::Thread" unless @thread.is_a?(Langchain::Thread)
50
56
 
51
57
  # The first message in the thread should be the system instructions
52
- # TODO: What if the user added old messages and the system instructions are already in there? Should this overwrite the existing instructions?
53
- initialize_instructions
54
58
  # For Google Gemini, and Anthropic system instructions are added to the `system:` param in the `chat` method
59
+ initialize_instructions
55
60
  end
56
61
 
57
62
  # Add a user message to the thread
@@ -105,6 +110,13 @@ module Langchain
105
110
  thread.messages
106
111
  end
107
112
 
113
+ # Run the assistant with automatic tool execution
114
+ #
115
+ # @return [Array<Langchain::Message>] The messages in the thread
116
+ def run!
117
+ run(auto_tool_execution: true)
118
+ end
119
+
108
120
  # Add a user message to the thread and run the assistant
109
121
  #
110
122
  # @param content [String] The content of the message
@@ -115,6 +127,14 @@ module Langchain
115
127
  run(auto_tool_execution: auto_tool_execution)
116
128
  end
117
129
 
130
+ # Add a user message to the thread and run the assistant with automatic tool execution
131
+ #
132
+ # @param content [String] The content of the message
133
+ # @return [Array<Langchain::Message>] The messages in the thread
134
+ def add_message_and_run!(content:)
135
+ add_message_and_run(content: content, auto_tool_execution: true)
136
+ end
137
+
118
138
  # Submit tool output to the thread
119
139
  #
120
140
  # @param tool_call_id [String] The ID of the tool call to submit output for
@@ -150,8 +170,21 @@ module Langchain
150
170
  thread.messages.unshift(message)
151
171
  end
152
172
 
173
+ def tool_choice=(new_tool_choice)
174
+ validate_tool_choice!(new_tool_choice)
175
+ @tool_choice = new_tool_choice
176
+ end
177
+
153
178
  private
154
179
 
180
+ # TODO: If tool_choice = "tool_function_name" and then tool is removed from the assistant, should we set tool_choice back to "auto"?
181
+ def validate_tool_choice!(tool_choice)
182
+ allowed_tool_choices = llm_adapter.allowed_tool_choices.concat(available_tool_names)
183
+ unless allowed_tool_choices.include?(tool_choice)
184
+ raise ArgumentError, "Tool choice must be one of: #{allowed_tool_choices.join(", ")}"
185
+ end
186
+ end
187
+
155
188
  # Check if the run is finished
156
189
  #
157
190
  # @param auto_tool_execution [Boolean] Whether or not to automatically run tools
@@ -257,20 +290,22 @@ module Langchain
257
290
  # @return [String] The tool role
258
291
  def determine_tool_role
259
292
  case llm
293
+ when Langchain::LLM::Anthropic
294
+ Langchain::Messages::AnthropicMessage::TOOL_ROLE
295
+ when Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI
296
+ Langchain::Messages::GoogleGeminiMessage::TOOL_ROLE
297
+ when Langchain::LLM::MistralAI
298
+ Langchain::Messages::MistralAIMessage::TOOL_ROLE
260
299
  when Langchain::LLM::Ollama
261
300
  Langchain::Messages::OllamaMessage::TOOL_ROLE
262
301
  when Langchain::LLM::OpenAI
263
302
  Langchain::Messages::OpenAIMessage::TOOL_ROLE
264
- when Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI
265
- Langchain::Messages::GoogleGeminiMessage::TOOL_ROLE
266
- when Langchain::LLM::Anthropic
267
- Langchain::Messages::AnthropicMessage::TOOL_ROLE
268
303
  end
269
304
  end
270
305
 
271
306
  def initialize_instructions
272
- if llm.is_a?(Langchain::LLM::OpenAI)
273
- add_message(role: "system", content: instructions) if instructions
307
+ if llm.is_a?(Langchain::LLM::OpenAI) || llm.is_a?(Langchain::LLM::MistralAI)
308
+ self.instructions = @instructions if @instructions
274
309
  end
275
310
  end
276
311
 
@@ -281,9 +316,10 @@ module Langchain
281
316
  Langchain.logger.info("Sending a call to #{llm.class}", for: self.class)
282
317
 
283
318
  params = @llm_adapter.build_chat_params(
284
- tools: @tools,
285
319
  instructions: @instructions,
286
- messages: thread.array_of_message_hashes
320
+ messages: thread.array_of_message_hashes,
321
+ tools: @tools,
322
+ tool_choice: tool_choice
287
323
  )
288
324
  @llm.chat(**params)
289
325
  end
@@ -298,7 +334,7 @@ module Langchain
298
334
 
299
335
  tool_instance = tools.find do |t|
300
336
  t.class.tool_name == tool_name
301
- end or raise ArgumentError, "Tool not found in assistant.tools"
337
+ end or raise ArgumentError, "Tool: #{tool_name} not found in assistant.tools"
302
338
 
303
339
  output = tool_instance.send(method_name, **tool_arguments)
304
340
 
@@ -329,20 +365,26 @@ module Langchain
329
365
  @total_tokens += total_tokens_from_operation if total_tokens_from_operation
330
366
  end
331
367
 
368
+ def available_tool_names
369
+ llm_adapter.available_tool_names(tools)
370
+ end
371
+
332
372
  # TODO: Fix the message truncation when context window is exceeded
333
373
 
334
374
  module LLM
335
375
  class Adapter
336
376
  def self.build(llm)
337
377
  case llm
378
+ when Langchain::LLM::Anthropic
379
+ Adapters::Anthropic.new
380
+ when Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI
381
+ Adapters::GoogleGemini.new
382
+ when Langchain::LLM::MistralAI
383
+ Adapters::MistralAI.new
338
384
  when Langchain::LLM::Ollama
339
385
  Adapters::Ollama.new
340
386
  when Langchain::LLM::OpenAI
341
387
  Adapters::OpenAI.new
342
- when Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI
343
- Adapters::GoogleGemini.new
344
- when Langchain::LLM::Anthropic
345
- Adapters::Anthropic.new
346
388
  else
347
389
  raise ArgumentError, "Unsupported LLM type: #{llm.class}"
348
390
  end
@@ -351,7 +393,7 @@ module Langchain
351
393
 
352
394
  module Adapters
353
395
  class Base
354
- def build_chat_params(tools:, instructions:, messages:)
396
+ def build_chat_params(tools:, instructions:, messages:, tool_choice:)
355
397
  raise NotImplementedError, "Subclasses must implement build_chat_params"
356
398
  end
357
399
 
@@ -365,10 +407,10 @@ module Langchain
365
407
  end
366
408
 
367
409
  class Ollama < Base
368
- def build_chat_params(tools:, instructions:, messages:)
410
+ def build_chat_params(tools:, instructions:, messages:, tool_choice:)
369
411
  params = {messages: messages}
370
412
  if tools.any?
371
- params[:tools] = tools.map { |tool| tool.class.function_schemas.to_openai_format }.flatten
413
+ params[:tools] = build_tools(tools)
372
414
  end
373
415
  params
374
416
  end
@@ -396,14 +438,28 @@ module Langchain
396
438
 
397
439
  [tool_call_id, tool_name, method_name, tool_arguments]
398
440
  end
441
+
442
+ def available_tool_names(tools)
443
+ build_tools(tools).map { |tool| tool.dig(:function, :name) }
444
+ end
445
+
446
+ def allowed_tool_choices
447
+ ["auto", "none"]
448
+ end
449
+
450
+ private
451
+
452
+ def build_tools(tools)
453
+ tools.map { |tool| tool.class.function_schemas.to_openai_format }.flatten
454
+ end
399
455
  end
400
456
 
401
457
  class OpenAI < Base
402
- def build_chat_params(tools:, instructions:, messages:)
458
+ def build_chat_params(tools:, instructions:, messages:, tool_choice:)
403
459
  params = {messages: messages}
404
460
  if tools.any?
405
- params[:tools] = tools.map { |tool| tool.class.function_schemas.to_openai_format }.flatten
406
- params[:tool_choice] = "auto"
461
+ params[:tools] = build_tools(tools)
462
+ params[:tool_choice] = build_tool_choice(tool_choice)
407
463
  end
408
464
  params
409
465
  end
@@ -431,15 +487,96 @@ module Langchain
431
487
 
432
488
  [tool_call_id, tool_name, method_name, tool_arguments]
433
489
  end
490
+
491
+ def build_tools(tools)
492
+ tools.map { |tool| tool.class.function_schemas.to_openai_format }.flatten
493
+ end
494
+
495
+ def allowed_tool_choices
496
+ ["auto", "none"]
497
+ end
498
+
499
+ def available_tool_names(tools)
500
+ build_tools(tools).map { |tool| tool.dig(:function, :name) }
501
+ end
502
+
503
+ private
504
+
505
+ def build_tool_choice(choice)
506
+ case choice
507
+ when "auto"
508
+ choice
509
+ else
510
+ {"type" => "function", "function" => {"name" => choice}}
511
+ end
512
+ end
513
+ end
514
+
515
+ class MistralAI < Base
516
+ def build_chat_params(tools:, instructions:, messages:, tool_choice:)
517
+ params = {messages: messages}
518
+ if tools.any?
519
+ params[:tools] = build_tools(tools)
520
+ params[:tool_choice] = build_tool_choice(tool_choice)
521
+ end
522
+ params
523
+ end
524
+
525
+ def build_message(role:, content: nil, tool_calls: [], tool_call_id: nil)
526
+ Langchain::Messages::MistralAIMessage.new(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
527
+ end
528
+
529
+ # Extract the tool call information from the OpenAI tool call hash
530
+ #
531
+ # @param tool_call [Hash] The tool call hash
532
+ # @return [Array] The tool call information
533
+ def extract_tool_call_args(tool_call:)
534
+ tool_call_id = tool_call.dig("id")
535
+
536
+ function_name = tool_call.dig("function", "name")
537
+ tool_name, method_name = function_name.split("__")
538
+
539
+ tool_arguments = tool_call.dig("function", "arguments")
540
+ tool_arguments = if tool_arguments.is_a?(Hash)
541
+ Langchain::Utils::HashTransformer.symbolize_keys(tool_arguments)
542
+ else
543
+ JSON.parse(tool_arguments, symbolize_names: true)
544
+ end
545
+
546
+ [tool_call_id, tool_name, method_name, tool_arguments]
547
+ end
548
+
549
+ def build_tools(tools)
550
+ tools.map { |tool| tool.class.function_schemas.to_openai_format }.flatten
551
+ end
552
+
553
+ def allowed_tool_choices
554
+ ["auto", "none"]
555
+ end
556
+
557
+ def available_tool_names(tools)
558
+ build_tools(tools).map { |tool| tool.dig(:function, :name) }
559
+ end
560
+
561
+ private
562
+
563
+ def build_tool_choice(choice)
564
+ case choice
565
+ when "auto"
566
+ choice
567
+ else
568
+ {"type" => "function", "function" => {"name" => choice}}
569
+ end
570
+ end
434
571
  end
435
572
 
436
573
  class GoogleGemini < Base
437
- def build_chat_params(tools:, instructions:, messages:)
574
+ def build_chat_params(tools:, instructions:, messages:, tool_choice:)
438
575
  params = {messages: messages}
439
576
  if tools.any?
440
- params[:tools] = tools.map { |tool| tool.class.function_schemas.to_google_gemini_format }.flatten
577
+ params[:tools] = build_tools(tools)
441
578
  params[:system] = instructions if instructions
442
- params[:tool_choice] = "auto"
579
+ params[:tool_choice] = build_tool_config(tool_choice)
443
580
  end
444
581
  params
445
582
  end
@@ -459,14 +596,39 @@ module Langchain
459
596
  tool_arguments = tool_call.dig("functionCall", "args").transform_keys(&:to_sym)
460
597
  [tool_call_id, tool_name, method_name, tool_arguments]
461
598
  end
599
+
600
+ def build_tools(tools)
601
+ tools.map { |tool| tool.class.function_schemas.to_google_gemini_format }.flatten
602
+ end
603
+
604
+ def allowed_tool_choices
605
+ ["auto", "none"]
606
+ end
607
+
608
+ def available_tool_names(tools)
609
+ build_tools(tools).map { |tool| tool.dig(:name) }
610
+ end
611
+
612
+ private
613
+
614
+ def build_tool_config(choice)
615
+ case choice
616
+ when "auto"
617
+ {function_calling_config: {mode: "auto"}}
618
+ when "none"
619
+ {function_calling_config: {mode: "none"}}
620
+ else
621
+ {function_calling_config: {mode: "any", allowed_function_names: [choice]}}
622
+ end
623
+ end
462
624
  end
463
625
 
464
626
  class Anthropic < Base
465
- def build_chat_params(tools:, instructions:, messages:)
627
+ def build_chat_params(tools:, instructions:, messages:, tool_choice:)
466
628
  params = {messages: messages}
467
629
  if tools.any?
468
- params[:tools] = tools.map { |tool| tool.class.function_schemas.to_anthropic_format }.flatten
469
- params[:tool_choice] = {type: "auto"}
630
+ params[:tools] = build_tools(tools)
631
+ params[:tool_choice] = build_tool_choice(tool_choice)
470
632
  end
471
633
  params[:system] = instructions if instructions
472
634
  params
@@ -487,6 +649,31 @@ module Langchain
487
649
  tool_arguments = tool_call.dig("input").transform_keys(&:to_sym)
488
650
  [tool_call_id, tool_name, method_name, tool_arguments]
489
651
  end
652
+
653
+ def build_tools(tools)
654
+ tools.map { |tool| tool.class.function_schemas.to_anthropic_format }.flatten
655
+ end
656
+
657
+ def allowed_tool_choices
658
+ ["auto", "any"]
659
+ end
660
+
661
+ def available_tool_names(tools)
662
+ build_tools(tools).map { |tool| tool.dig(:name) }
663
+ end
664
+
665
+ private
666
+
667
+ def build_tool_choice(choice)
668
+ case choice
669
+ when "auto"
670
+ {type: "auto"}
671
+ when "any"
672
+ {type: "any"}
673
+ else
674
+ {type: "tool", name: choice}
675
+ end
676
+ end
490
677
  end
491
678
  end
492
679
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langchain
4
+ module Messages
5
+ class MistralAIMessage < Base
6
+ # MistralAI 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 MistralAI 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) # TODO: Implement image_file: reference (https://platform.openai.com/docs/api-reference/messages/object#messages/object-content)
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
+ # Check if the message came from an LLM
34
+ #
35
+ # @return [Boolean] true/false whether this message was produced by an LLM
36
+ def llm?
37
+ assistant?
38
+ end
39
+
40
+ # Convert the message to an MistralAI API-compatible hash
41
+ #
42
+ # @return [Hash] The message as an MistralAI API-compatible hash
43
+ def to_hash
44
+ {}.tap do |h|
45
+ h[:role] = role
46
+ h[:content] = content if content # Content is nil for tool calls
47
+ h[:tool_calls] = tool_calls if tool_calls.any?
48
+ h[:tool_call_id] = tool_call_id if tool_call_id
49
+ end
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
@@ -4,12 +4,14 @@ module Langchain
4
4
  # Langchain::Thread keeps track of messages in a conversation.
5
5
  # TODO: Add functionality to persist to the thread to disk, DB, storage, etc.
6
6
  class Thread
7
- attr_accessor :messages
7
+ attr_accessor :messages, :add_message_callback
8
8
 
9
9
  # @param messages [Array<Langchain::Message>]
10
- def initialize(messages: [])
10
+ # @param add_message_callback [Proc] A callback to call when a message is added to the thread
11
+ def initialize(messages: [], add_message_callback: nil)
11
12
  raise ArgumentError, "messages array must only contain Langchain::Message instance(s)" unless messages.is_a?(Array) && messages.all? { |m| m.is_a?(Langchain::Messages::Base) }
12
13
 
14
+ @add_message_callback = add_message_callback
13
15
  @messages = messages
14
16
  end
15
17
 
@@ -34,6 +36,9 @@ module Langchain
34
36
  def add_message(message)
35
37
  raise ArgumentError, "message must be a Langchain::Message instance" unless message.is_a?(Langchain::Messages::Base)
36
38
 
39
+ # Call the callback with the message
40
+ add_message_callback.call(message) if add_message_callback # rubocop:disable Style/SafeNavigation
41
+
37
42
  # Prepend the message to the thread
38
43
  messages << message
39
44
  end
@@ -33,8 +33,11 @@ module Langchain::LLM
33
33
  )
34
34
  @defaults = DEFAULTS.merge(default_options)
35
35
  chat_parameters.update(
36
+ model: {default: @defaults[:chat_completion_model_name]},
36
37
  logprobs: {},
37
38
  top_logprobs: {},
39
+ n: {default: @defaults[:n]},
40
+ temperature: {default: @defaults[:temperature]},
38
41
  user: {}
39
42
  )
40
43
  chat_parameters.ignore(:top_k)
@@ -39,7 +39,6 @@ module Langchain::LLM
39
39
  def chat(params = {})
40
40
  params[:system] = {parts: [{text: params[:system]}]} if params[:system]
41
41
  params[:tools] = {function_declarations: params[:tools]} if params[:tools]
42
- params[:tool_choice] = {function_calling_config: {mode: params[:tool_choice].upcase}} if params[:tool_choice]
43
42
 
44
43
  raise ArgumentError.new("messages argument is required") if Array(params[:messages]).empty?
45
44
 
@@ -8,7 +8,7 @@ module Langchain::LLM
8
8
  # llm = Langchain::LLM::MistralAI.new(api_key: ENV["MISTRAL_AI_API_KEY"])
9
9
  class MistralAI < Base
10
10
  DEFAULTS = {
11
- chat_completion_model_name: "mistral-medium",
11
+ chat_completion_model_name: "mistral-large-latest",
12
12
  embeddings_model_name: "mistral-embed"
13
13
  }.freeze
14
14
 
@@ -218,8 +218,8 @@ module Langchain::LLM
218
218
  top_p: nil
219
219
  )
220
220
  parameters = {
221
- prompt: text,
222
- model: model
221
+ model: model,
222
+ input: Array(text)
223
223
  }.compact
224
224
 
225
225
  llm_parameters = {
@@ -243,7 +243,7 @@ module Langchain::LLM
243
243
 
244
244
  parameters[:options] = llm_parameters.compact
245
245
 
246
- response = client.post("api/embeddings") do |req|
246
+ response = client.post("api/embed") do |req|
247
247
  req.body = parameters
248
248
  end
249
249
 
@@ -7,7 +7,15 @@ module Langchain::LLM
7
7
  end
8
8
 
9
9
  def chat_completion
10
- raw_response.dig("choices", 0, "message", "content")
10
+ chat_completions.dig(0, "message", "content")
11
+ end
12
+
13
+ def chat_completions
14
+ raw_response.dig("choices")
15
+ end
16
+
17
+ def tool_calls
18
+ chat_completions.dig(0, "message", "tool_calls") || []
11
19
  end
12
20
 
13
21
  def role
@@ -28,7 +28,7 @@ module Langchain::LLM
28
28
  end
29
29
 
30
30
  def embeddings
31
- [raw_response&.dig("embedding")]
31
+ raw_response&.dig("embeddings") || []
32
32
  end
33
33
 
34
34
  def role
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
3
  require "json-schema"
5
4
 
6
5
  module Langchain::OutputParsers
@@ -1,4 +1,4 @@
1
- require "uri"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Langchain
4
4
  module Processors
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "strscan"
4
- require "json"
5
4
  require "yaml"
6
5
 
7
6
  module Langchain::Prompt
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "strscan"
4
4
  require "pathname"
5
- require "json"
6
5
  require "yaml"
7
6
 
8
7
  module Langchain::Prompt
@@ -58,8 +58,9 @@ module Langchain::Prompt
58
58
  #
59
59
  def format(**kwargs)
60
60
  result = @template
61
+ result = result.gsub(/{{/, "{").gsub(/}}/, "}")
61
62
  kwargs.each { |key, value| result = result.gsub(/\{#{key}\}/, value.to_s) }
62
- result.gsub(/{{/, "{").gsub(/}}/, "}")
63
+ result
63
64
  end
64
65
 
65
66
  #
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Langchain::Tool
2
4
  #
3
- # Connects to a database, executes SQL queries, and outputs DB schema for Agents to use
5
+ # Connects to a SQL database, executes SQL queries, and outputs DB schema for Agents to use
4
6
  #
5
7
  # Gem requirements:
6
8
  # gem "sequel", "~> 5.68.0"
@@ -15,7 +17,9 @@ module Langchain::Tool
15
17
  define_function :list_tables, description: "Database Tool: Returns a list of tables in the database"
16
18
 
17
19
  define_function :describe_tables, description: "Database Tool: Returns the schema for a list of tables" do
18
- property :tables, type: "string", description: "The tables to describe", required: true
20
+ property :tables, type: "array", description: "The tables to describe", required: true do
21
+ item type: "string"
22
+ end
19
23
  end
20
24
 
21
25
  define_function :dump_schema, description: "Database Tool: Returns the database schema"
@@ -38,25 +42,32 @@ module Langchain::Tool
38
42
  raise StandardError, "connection_string parameter cannot be blank" if connection_string.empty?
39
43
 
40
44
  @db = Sequel.connect(connection_string)
45
+ # TODO: This is a bug, these 2 parameters are completely ignored.
41
46
  @requested_tables = tables
42
47
  @excluded_tables = exclude_tables
43
48
  end
44
49
 
45
50
  # Database Tool: Returns a list of tables in the database
51
+ #
52
+ # @return [Array<Symbol>] List of tables in the database
46
53
  def list_tables
47
54
  db.tables
48
55
  end
49
56
 
50
57
  # Database Tool: Returns the schema for a list of tables
51
58
  #
52
- # @param tables [String] The tables to describe.
53
- # @return [String] Database schema for the tables
54
- def describe_tables(tables:)
55
- schema = ""
56
- tables.split(",").each do |table|
57
- describe_table(table, schema)
58
- end
59
- schema
59
+ # @param tables [Array<String>] The tables to describe.
60
+ # @return [String] The schema for the tables
61
+ def describe_tables(tables: [])
62
+ return "No tables specified" if tables.empty?
63
+
64
+ Langchain.logger.info("Describing tables: #{tables}", for: self.class)
65
+
66
+ tables
67
+ .map do |table|
68
+ describe_table(table)
69
+ end
70
+ .join("\n")
60
71
  end
61
72
 
62
73
  # Database Tool: Returns the database schema
@@ -64,18 +75,39 @@ module Langchain::Tool
64
75
  # @return [String] Database schema
65
76
  def dump_schema
66
77
  Langchain.logger.info("Dumping schema tables and keys", for: self.class)
67
- schema = ""
68
- db.tables.each do |table|
69
- describe_table(table, schema)
78
+
79
+ schemas = db.tables.map do |table|
80
+ describe_table(table)
70
81
  end
71
- schema
82
+ schemas.join("\n")
72
83
  end
73
84
 
74
- def describe_table(table, schema)
85
+ # Database Tool: Executes a SQL query and returns the results
86
+ #
87
+ # @param input [String] SQL query to be executed
88
+ # @return [Array] Results from the SQL query
89
+ def execute(input:)
90
+ Langchain.logger.info("Executing \"#{input}\"", for: self.class)
91
+
92
+ db[input].to_a
93
+ rescue Sequel::DatabaseError => e
94
+ Langchain.logger.error(e.message, for: self.class)
95
+ e.message # Return error to LLM
96
+ end
97
+
98
+ private
99
+
100
+ # Describes a table and its schema
101
+ #
102
+ # @param table [String] The table to describe
103
+ # @return [String] The schema for the table
104
+ def describe_table(table)
105
+ # TODO: There's probably a clear way to do all of this below
106
+
75
107
  primary_key_columns = []
76
108
  primary_key_column_count = db.schema(table).count { |column| column[1][:primary_key] == true }
77
109
 
78
- schema << "CREATE TABLE #{table}(\n"
110
+ schema = "CREATE TABLE #{table}(\n"
79
111
  db.schema(table).each do |column|
80
112
  schema << "#{column[0]} #{column[1][:type]}"
81
113
  if column[1][:primary_key] == true
@@ -95,17 +127,5 @@ module Langchain::Tool
95
127
  end
96
128
  schema << ");\n"
97
129
  end
98
-
99
- # Database Tool: Executes a SQL query and returns the results
100
- #
101
- # @param input [String] SQL query to be executed
102
- # @return [Array] Results from the SQL query
103
- def execute(input:)
104
- Langchain.logger.info("Executing \"#{input}\"", for: self.class)
105
-
106
- db[input].to_a
107
- rescue Sequel::DatabaseError => e
108
- Langchain.logger.error(e.message, for: self.class)
109
- end
110
130
  end
111
131
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
-
5
3
  #
6
4
  # Extends a class to be used as a tool in the assistant.
7
5
  # A tool is a collection of functions (methods) used to perform specific tasks.
@@ -42,8 +40,8 @@ module Langchain::ToolDefinition
42
40
  # @param method_name [Symbol] Name of the method to define
43
41
  # @param description [String] Description of the function
44
42
  # @yield Block that defines the parameters for the function
45
- def define_function(method_name, description:, &)
46
- function_schemas.add_function(method_name:, description:, &)
43
+ def define_function(method_name, description:, &block)
44
+ function_schemas.add_function(method_name:, description:, &block)
47
45
  end
48
46
 
49
47
  # Returns the FunctionSchemas instance for this tool
@@ -76,11 +74,11 @@ module Langchain::ToolDefinition
76
74
  # @param description [String] Description of the function
77
75
  # @yield Block that defines the parameters for the function
78
76
  # @raise [ArgumentError] If a block is defined and no parameters are specified for the function
79
- def add_function(method_name:, description:, &)
77
+ def add_function(method_name:, description:, &block)
80
78
  name = "#{@tool_name}__#{method_name}"
81
79
 
82
- if block_given?
83
- parameters = ParameterBuilder.new(parent_type: "object").build(&)
80
+ if block_given? # rubocop:disable Performance/BlockGivenWithExplicitBlock
81
+ parameters = ParameterBuilder.new(parent_type: "object").build(&block)
84
82
 
85
83
  if parameters[:properties].empty?
86
84
  raise ArgumentError, "Function parameters must have at least one property defined within it, if a block is provided"
@@ -130,8 +128,8 @@ module Langchain::ToolDefinition
130
128
  #
131
129
  # @yield Block that defines the properties of the schema
132
130
  # @return [Hash] The built schema
133
- def build(&)
134
- instance_eval(&)
131
+ def build(&block)
132
+ instance_eval(&block)
135
133
  @schema
136
134
  end
137
135
 
@@ -144,13 +142,13 @@ module Langchain::ToolDefinition
144
142
  # @param required [Boolean] Whether the property is required
145
143
  # @yield [Block] Block for nested properties (only for object and array types)
146
144
  # @raise [ArgumentError] If any parameter is invalid
147
- def property(name = nil, type:, description: nil, enum: nil, required: false, &)
145
+ def property(name = nil, type:, description: nil, enum: nil, required: false, &block)
148
146
  validate_parameters(name:, type:, enum:, required:)
149
147
 
150
148
  prop = {type:, description:, enum:}.compact
151
149
 
152
- if block_given?
153
- nested_schema = ParameterBuilder.new(parent_type: type).build(&)
150
+ if block_given? # rubocop:disable Performance/BlockGivenWithExplicitBlock
151
+ nested_schema = ParameterBuilder.new(parent_type: type).build(&block)
154
152
 
155
153
  case type
156
154
  when "object"
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "securerandom"
4
- require "json"
5
4
  require "timeout"
6
- require "uri"
7
5
 
8
6
  module Langchain::Vectorsearch
9
7
  class Epsilla < Base
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Langchain
4
- VERSION = "0.15.3"
4
+ VERSION = "0.15.5"
5
5
  end
data/lib/langchain.rb CHANGED
@@ -4,6 +4,9 @@ require "logger"
4
4
  require "pathname"
5
5
  require "rainbow"
6
6
  require "zeitwerk"
7
+ require "uri"
8
+ require "json"
9
+
7
10
  loader = Zeitwerk::Loader.for_gem
8
11
  loader.ignore("#{__dir__}/langchainrb.rb")
9
12
  loader.inflector.inflect(
@@ -18,6 +21,7 @@ loader.inflector.inflect(
18
21
  "llm" => "LLM",
19
22
  "mistral_ai" => "MistralAI",
20
23
  "mistral_ai_response" => "MistralAIResponse",
24
+ "mistral_ai_message" => "MistralAIMessage",
21
25
  "openai" => "OpenAI",
22
26
  "openai_validator" => "OpenAIValidator",
23
27
  "openai_response" => "OpenAIResponse",
@@ -29,6 +33,7 @@ loader.collapse("#{__dir__}/langchain/assistants")
29
33
 
30
34
  loader.collapse("#{__dir__}/langchain/tool/calculator")
31
35
  loader.collapse("#{__dir__}/langchain/tool/database")
36
+ loader.collapse("#{__dir__}/langchain/tool/docs_tool")
32
37
  loader.collapse("#{__dir__}/langchain/tool/file_system")
33
38
  loader.collapse("#{__dir__}/langchain/tool/google_search")
34
39
  loader.collapse("#{__dir__}/langchain/tool/ruby_code_interpreter")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: langchainrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.3
4
+ version: 0.15.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Bondarev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-27 00:00:00.000000000 Z
11
+ date: 2024-09-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: baran
@@ -669,6 +669,7 @@ files:
669
669
  - lib/langchain/assistants/messages/anthropic_message.rb
670
670
  - lib/langchain/assistants/messages/base.rb
671
671
  - lib/langchain/assistants/messages/google_gemini_message.rb
672
+ - lib/langchain/assistants/messages/mistral_ai_message.rb
672
673
  - lib/langchain/assistants/messages/ollama_message.rb
673
674
  - lib/langchain/assistants/messages/openai_message.rb
674
675
  - lib/langchain/assistants/thread.rb