langchainrb 0.19.3 → 0.19.4

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: 5567aa14daf63f120a6c2b2b2991fbd85b650b4cb4e2ac49703736dacd46197e
4
- data.tar.gz: ee71c45590998793c343c9620565cfcfdff5cd2d715091f3ad6804d54b814444
3
+ metadata.gz: 65a5c3e4eb0dcf204f75f20fde73d4e049fe657177e19d3fbcc8ffefad059776
4
+ data.tar.gz: 167a2e8c9305859afc8d384a3b2ff33ea7fc77f7abe7feb93a8f6c104effd2f3
5
5
  SHA512:
6
- metadata.gz: a5d212aadfef3ca07a6c9dc517141f0e6dedb1e414b0e8bbcbc0b7abddd5e2405a8a07ab6cc10efd66ee8997c41b5c8bd82ea8755d0f8e5fea7728db94f82379
7
- data.tar.gz: a3a5a5c62cc075e8d09c0e6b629e1ec8b118d35d98e4de107c48d779d8b35261de3ad0061e22a4be4b2aefc15bbb25c20f658fe1f0a694ecc87279cd21309ff8
6
+ metadata.gz: 7b65c3e5ef0f7774f634157a98e2fdb2db57fcb9981c96c0ae476ef78ee4a4a3d4822c48339543b0cd0e4d708bf043f2d9653fdfdda5f0df093ea01839d033e3
7
+ data.tar.gz: e93b0e741cf729697067a4d89b0c938e8261cc8df3d0b30dc0a1dbbb03a0f6100ede5120e2ba8f2d7cc65cf6b5707061d64a4b6e101020413713ef445c0265be
data/CHANGELOG.md CHANGED
@@ -11,6 +11,10 @@
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.19.4] - 2025-02-17
15
+ - [BREAKING] [https://github.com/patterns-ai-core/langchainrb/pull/894] Tools can now output image_urls, and all tool output must be wrapped by a tool_response() method
16
+ - [BUGFIX] [https://github.com/patterns-ai-core/langchainrb/pull/921] Fix for Assistant when OpenAI o1/o3 models are used
17
+
14
18
  ## [0.19.3] - 2025-01-13
15
19
  - [BUGFIX] [https://github.com/patterns-ai-core/langchainrb/pull/900] Empty text content should not be set when content is nil when using AnthropicMessage
16
20
 
data/README.md CHANGED
@@ -580,11 +580,11 @@ class MovieInfoTool
580
580
  end
581
581
 
582
582
  def search_movie(query:)
583
- ...
583
+ tool_response(...)
584
584
  end
585
585
 
586
586
  def get_movie_details(movie_id:)
587
- ...
587
+ tool_response(...)
588
588
  end
589
589
  end
590
590
  ```
@@ -24,7 +24,9 @@ module Langchain
24
24
  if tools.any?
25
25
  params[:tools] = build_tools(tools)
26
26
  params[:tool_choice] = build_tool_choice(tool_choice)
27
- params[:parallel_tool_calls] = parallel_tool_calls
27
+ # Temporary fix because OpenAI o1/o3/reasoning models don't support `parallel_tool_calls` parameter.
28
+ # Set `Assistant.new(parallel_tool_calls: nil, ...)` to avoid the error.
29
+ params[:parallel_tool_calls] = parallel_tool_calls unless parallel_tool_calls.nil?
28
30
  end
29
31
  params
30
32
  end
@@ -371,9 +371,15 @@ module Langchain
371
371
 
372
372
  # Call the callback if set
373
373
  tool_execution_callback.call(tool_call_id, tool_name, method_name, tool_arguments) if tool_execution_callback # rubocop:disable Style/SafeNavigation
374
+
374
375
  output = tool_instance.send(method_name, **tool_arguments)
375
376
 
376
- submit_tool_output(tool_call_id: tool_call_id, output: output)
377
+ # Handle both ToolResponse and legacy return values
378
+ if output.is_a?(ToolResponse)
379
+ add_message(role: @llm_adapter.tool_role, content: output.content, image_url: output.image_url, tool_call_id: tool_call_id)
380
+ else
381
+ submit_tool_output(tool_call_id: tool_call_id, output: output)
382
+ end
377
383
  end
378
384
 
379
385
  # Build a message
@@ -28,15 +28,4 @@ module Langchain::OutputParsers
28
28
  raise NotImplementedError
29
29
  end
30
30
  end
31
-
32
- class OutputParserException < StandardError
33
- def initialize(message, text)
34
- @message = message
35
- @text = text
36
- end
37
-
38
- def to_s
39
- "#{@message}\nText: #{@text}"
40
- end
41
- end
42
31
  end
@@ -0,0 +1,10 @@
1
+ class Langchain::OutputParsers::OutputParserException < StandardError
2
+ def initialize(message, text)
3
+ @message = message
4
+ @text = text
5
+ end
6
+
7
+ def to_s
8
+ "#{@message}\nText: #{@text}"
9
+ end
10
+ end
@@ -26,13 +26,14 @@ module Langchain::Tool
26
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
27
  #
28
28
  # @param input [String] math expression
29
- # @return [String] Answer
29
+ # @return [Langchain::Tool::Response] Answer
30
30
  def execute(input:)
31
31
  Langchain.logger.debug("#{self.class} - Executing \"#{input}\"")
32
32
 
33
- Eqn::Calculator.calc(input)
33
+ result = Eqn::Calculator.calc(input)
34
+ tool_response(content: result)
34
35
  rescue Eqn::ParseError, Eqn::NoVariableValueError
35
- "\"#{input}\" is an invalid mathematical expression"
36
+ tool_response(content: "\"#{input}\" is an invalid mathematical expression")
36
37
  end
37
38
  end
38
39
  end
@@ -49,50 +49,53 @@ module Langchain::Tool
49
49
 
50
50
  # Database Tool: Returns a list of tables in the database
51
51
  #
52
- # @return [Array<Symbol>] List of tables in the database
52
+ # @return [Langchain::Tool::Response] List of tables in the database
53
53
  def list_tables
54
- db.tables
54
+ tool_response(content: db.tables)
55
55
  end
56
56
 
57
57
  # Database Tool: Returns the schema for a list of tables
58
58
  #
59
59
  # @param tables [Array<String>] The tables to describe.
60
- # @return [String] The schema for the tables
60
+ # @return [Langchain::Tool::Response] The schema for the tables
61
61
  def describe_tables(tables: [])
62
62
  return "No tables specified" if tables.empty?
63
63
 
64
64
  Langchain.logger.debug("#{self.class} - Describing tables: #{tables}")
65
65
 
66
- tables
66
+ result = tables
67
67
  .map do |table|
68
68
  describe_table(table)
69
69
  end
70
70
  .join("\n")
71
+
72
+ tool_response(content: result)
71
73
  end
72
74
 
73
75
  # Database Tool: Returns the database schema
74
76
  #
75
- # @return [String] Database schema
77
+ # @return [Langchain::Tool::Response] Database schema
76
78
  def dump_schema
77
79
  Langchain.logger.debug("#{self.class} - Dumping schema tables and keys")
78
80
 
79
81
  schemas = db.tables.map do |table|
80
82
  describe_table(table)
81
83
  end
82
- schemas.join("\n")
84
+
85
+ tool_response(content: schemas.join("\n"))
83
86
  end
84
87
 
85
88
  # Database Tool: Executes a SQL query and returns the results
86
89
  #
87
90
  # @param input [String] SQL query to be executed
88
- # @return [Array] Results from the SQL query
91
+ # @return [Langchain::Tool::Response] Results from the SQL query
89
92
  def execute(input:)
90
93
  Langchain.logger.debug("#{self.class} - Executing \"#{input}\"")
91
94
 
92
- db[input].to_a
95
+ tool_response(content: db[input].to_a)
93
96
  rescue Sequel::DatabaseError => e
94
97
  Langchain.logger.error("#{self.class} - #{e.message}")
95
- e.message # Return error to LLM
98
+ tool_response(content: e.message)
96
99
  end
97
100
 
98
101
  private
@@ -100,7 +103,7 @@ module Langchain::Tool
100
103
  # Describes a table and its schema
101
104
  #
102
105
  # @param table [String] The table to describe
103
- # @return [String] The schema for the table
106
+ # @return [Langchain::Tool::Response] The schema for the table
104
107
  def describe_table(table)
105
108
  # TODO: There's probably a clear way to do all of this below
106
109
 
@@ -127,6 +130,8 @@ module Langchain::Tool
127
130
  schema << ",\n" unless fk == db.foreign_key_list(table).last
128
131
  end
129
132
  schema << ");\n"
133
+
134
+ tool_response(content: schema)
130
135
  end
131
136
  end
132
137
  end
@@ -24,21 +24,22 @@ module Langchain::Tool
24
24
  end
25
25
 
26
26
  def list_directory(directory_path:)
27
- Dir.entries(directory_path)
27
+ tool_response(content: Dir.entries(directory_path))
28
28
  rescue Errno::ENOENT
29
- "No such directory: #{directory_path}"
29
+ tool_response(content: "No such directory: #{directory_path}")
30
30
  end
31
31
 
32
32
  def read_file(file_path:)
33
- File.read(file_path)
33
+ tool_response(content: File.read(file_path))
34
34
  rescue Errno::ENOENT
35
- "No such file: #{file_path}"
35
+ tool_response(content: "No such file: #{file_path}")
36
36
  end
37
37
 
38
38
  def write_to_file(file_path:, content:)
39
39
  File.write(file_path, content)
40
+ tool_response(content: "File written successfully")
40
41
  rescue Errno::EACCES
41
- "Permission denied: #{file_path}"
42
+ tool_response(content: "Permission denied: #{file_path}")
42
43
  end
43
44
  end
44
45
  end
@@ -36,7 +36,7 @@ module Langchain::Tool
36
36
  # Executes Google Search and returns the result
37
37
  #
38
38
  # @param input [String] search query
39
- # @return [String] Answer
39
+ # @return [Langchain::Tool::Response] Answer
40
40
  def execute(input:)
41
41
  Langchain.logger.debug("#{self.class} - Executing \"#{input}\"")
42
42
 
@@ -44,31 +44,31 @@ module Langchain::Tool
44
44
 
45
45
  answer_box = results[:answer_box_list] ? results[:answer_box_list].first : results[:answer_box]
46
46
  if answer_box
47
- return answer_box[:result] ||
47
+ return tool_response(content: answer_box[:result] ||
48
48
  answer_box[:answer] ||
49
49
  answer_box[:snippet] ||
50
50
  answer_box[:snippet_highlighted_words] ||
51
- answer_box.reject { |_k, v| v.is_a?(Hash) || v.is_a?(Array) || v.start_with?("http") }
51
+ answer_box.reject { |_k, v| v.is_a?(Hash) || v.is_a?(Array) || v.start_with?("http") })
52
52
  elsif (events_results = results[:events_results])
53
- return events_results.take(10)
53
+ return tool_response(content: events_results.take(10))
54
54
  elsif (sports_results = results[:sports_results])
55
- return sports_results
55
+ return tool_response(content: sports_results)
56
56
  elsif (top_stories = results[:top_stories])
57
- return top_stories
57
+ return tool_response(content: top_stories)
58
58
  elsif (news_results = results[:news_results])
59
- return news_results
59
+ return tool_response(content: news_results)
60
60
  elsif (jobs_results = results.dig(:jobs_results, :jobs))
61
- return jobs_results
61
+ return tool_response(content: jobs_results)
62
62
  elsif (shopping_results = results[:shopping_results]) && shopping_results.first.key?(:title)
63
- return shopping_results.take(3)
63
+ return tool_response(content: shopping_results.take(3))
64
64
  elsif (questions_and_answers = results[:questions_and_answers])
65
- return questions_and_answers
65
+ return tool_response(content: questions_and_answers)
66
66
  elsif (popular_destinations = results.dig(:popular_destinations, :destinations))
67
- return popular_destinations
67
+ return tool_response(content: popular_destinations)
68
68
  elsif (top_sights = results.dig(:top_sights, :sights))
69
- return top_sights
69
+ return tool_response(content: top_sights)
70
70
  elsif (images_results = results[:images_results]) && images_results.first.key?(:thumbnail)
71
- return images_results.map { |h| h[:thumbnail] }.take(10)
71
+ return tool_response(content: images_results.map { |h| h[:thumbnail] }.take(10))
72
72
  end
73
73
 
74
74
  snippets = []
@@ -110,8 +110,8 @@ module Langchain::Tool
110
110
  snippets << local_results
111
111
  end
112
112
 
113
- return "No good search result found" if snippets.empty?
114
- snippets
113
+ return tool_response(content: "No good search result found") if snippets.empty?
114
+ tool_response(content: snippets)
115
115
  end
116
116
 
117
117
  #
@@ -57,7 +57,7 @@ module Langchain::Tool
57
57
  # @param page_size [Integer] The number of results to return per page. 20 is the API's default, 100 is the maximum. Our default is 5.
58
58
  # @param page [Integer] Use this to page through the results.
59
59
  #
60
- # @return [String] JSON response
60
+ # @return [Langchain::Tool::Response] JSON response
61
61
  def get_everything(
62
62
  q: nil,
63
63
  search_in: nil,
@@ -86,7 +86,8 @@ module Langchain::Tool
86
86
  params[:pageSize] = page_size if page_size
87
87
  params[:page] = page if page
88
88
 
89
- send_request(path: "everything", params: params)
89
+ response = send_request(path: "everything", params: params)
90
+ tool_response(content: response)
90
91
  end
91
92
 
92
93
  # Retrieve top headlines
@@ -98,7 +99,7 @@ module Langchain::Tool
98
99
  # @param page_size [Integer] The number of results to return per page. 20 is the API's default, 100 is the maximum. Our default is 5.
99
100
  # @param page [Integer] Use this to page through the results.
100
101
  #
101
- # @return [String] JSON response
102
+ # @return [Langchain::Tool::Response] JSON response
102
103
  def get_top_headlines(
103
104
  country: nil,
104
105
  category: nil,
@@ -117,7 +118,8 @@ module Langchain::Tool
117
118
  params[:pageSize] = page_size if page_size
118
119
  params[:page] = page if page
119
120
 
120
- send_request(path: "top-headlines", params: params)
121
+ response = send_request(path: "top-headlines", params: params)
122
+ tool_response(content: response)
121
123
  end
122
124
 
123
125
  # Retrieve news sources
@@ -126,7 +128,7 @@ module Langchain::Tool
126
128
  # @param language [String] The 2-letter ISO-639-1 code of the language you want to get headlines for. Possible options: ar, de, en, es, fr, he, it, nl, no, pt, ru, se, ud, zh.
127
129
  # @param country [String] The 2-letter ISO 3166-1 code of the country you want to get headlines for. Possible options: ae, ar, at, au, be, bg, br, ca, ch, cn, co, cu, cz, de, eg, fr, gb, gr, hk, hu, id, ie, il, in, it, jp, kr, lt, lv, ma, mx, my, ng, nl, no, nz, ph, pl, pt, ro, rs, ru, sa, se, sg, si, sk, th, tr, tw, ua, us, ve, za.
128
130
  #
129
- # @return [String] JSON response
131
+ # @return [Langchain::Tool::Response] JSON response
130
132
  def get_sources(
131
133
  category: nil,
132
134
  language: nil,
@@ -139,7 +141,8 @@ module Langchain::Tool
139
141
  params[:category] = category if category
140
142
  params[:language] = language if language
141
143
 
142
- send_request(path: "top-headlines/sources", params: params)
144
+ response = send_request(path: "top-headlines/sources", params: params)
145
+ tool_response(content: response)
143
146
  end
144
147
 
145
148
  private
@@ -27,11 +27,11 @@ module Langchain::Tool
27
27
  # Executes Ruby code in a sandboxes environment.
28
28
  #
29
29
  # @param input [String] ruby code expression
30
- # @return [String] Answer
30
+ # @return [Langchain::Tool::Response] Answer
31
31
  def execute(input:)
32
32
  Langchain.logger.debug("#{self.class} - Executing \"#{input}\"")
33
33
 
34
- safe_eval(input)
34
+ tool_response(content: safe_eval(input))
35
35
  end
36
36
 
37
37
  def safe_eval(code)
@@ -41,7 +41,7 @@ module Langchain::Tool
41
41
  # @param include_domains [Array<String>] A list of domains to specifically include in the search results. Default is None, which includes all domains.
42
42
  # @param exclude_domains [Array<String>] A list of domains to specifically exclude from the search results. Default is None, which doesn't exclude any domains.
43
43
  #
44
- # @return [String] The search results in JSON format.
44
+ # @return [Langchain::Tool::Response] The search results in JSON format.
45
45
  def search(
46
46
  query:,
47
47
  search_depth: "basic",
@@ -70,7 +70,7 @@ module Langchain::Tool
70
70
  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
71
71
  http.request(request)
72
72
  end
73
- response.body
73
+ tool_response(content: response.body)
74
74
  end
75
75
  end
76
76
  end
@@ -33,8 +33,10 @@ module Langchain::Tool
33
33
  #
34
34
  # @param query [String] The query to search for
35
35
  # @param k [Integer] The number of results to return
36
+ # @return [Langchain::Tool::Response] The response from the server
36
37
  def similarity_search(query:, k: 4)
37
- vectorsearch.similarity_search(query:, k: 4)
38
+ result = vectorsearch.similarity_search(query:, k: 4)
39
+ tool_response(content: result)
38
40
  end
39
41
  end
40
42
  end
@@ -55,15 +55,15 @@ module Langchain::Tool
55
55
  params = {appid: @api_key, q: [city, state_code, country_code].compact.join(","), units: units}
56
56
 
57
57
  location_response = send_request(path: "geo/1.0/direct", params: params.except(:units))
58
- return location_response if location_response.is_a?(String) # Error occurred
58
+ return tool_response(content: location_response) if location_response.is_a?(String) # Error occurred
59
59
 
60
60
  location = location_response.first
61
- return "Location not found" unless location
61
+ return tool_response(content: "Location not found") unless location
62
62
 
63
63
  params = params.merge(lat: location["lat"], lon: location["lon"]).except(:q)
64
64
  weather_data = send_request(path: "data/2.5/weather", params: params)
65
65
 
66
- parse_weather_response(weather_data, units)
66
+ tool_response(content: parse_weather_response(weather_data, units))
67
67
  end
68
68
 
69
69
  def send_request(path:, params:)
@@ -27,13 +27,13 @@ module Langchain::Tool
27
27
  # Executes Wikipedia API search and returns the answer
28
28
  #
29
29
  # @param input [String] search query
30
- # @return [String] Answer
30
+ # @return [Langchain::Tool::Response] Answer
31
31
  def execute(input:)
32
32
  Langchain.logger.debug("#{self.class} - Executing \"#{input}\"")
33
33
 
34
34
  page = ::Wikipedia.find(input)
35
35
  # It would be nice to figure out a way to provide page.content but the LLM token limit is an issue
36
- page.summary
36
+ tool_response(content: page.summary)
37
37
  end
38
38
  end
39
39
  end
@@ -61,6 +61,20 @@ module Langchain::ToolDefinition
61
61
  .downcase
62
62
  end
63
63
 
64
+ def self.extended(base)
65
+ base.include(InstanceMethods)
66
+ end
67
+
68
+ module InstanceMethods
69
+ # Create a tool response
70
+ # @param content [String, nil] The content of the tool response
71
+ # @param image_url [String, nil] The URL of an image
72
+ # @return [Langchain::ToolResponse] The tool response
73
+ def tool_response(content: nil, image_url: nil)
74
+ Langchain::ToolResponse.new(content: content, image_url: image_url)
75
+ end
76
+ end
77
+
64
78
  # Manages schemas for functions
65
79
  class FunctionSchemas
66
80
  def initialize(tool_name)
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langchain
4
+ # ToolResponse represents the standardized output of a tool.
5
+ # It can contain either text content or an image URL.
6
+ class ToolResponse
7
+ attr_reader :content, :image_url
8
+
9
+ # Initializes a new ToolResponse.
10
+ #
11
+ # @param content [String] The text content of the response.
12
+ # @param image_url [String, nil] Optional URL to an image.
13
+ def initialize(content: nil, image_url: nil)
14
+ raise ArgumentError, "Either content or image_url must be provided" if content.nil? && image_url.nil?
15
+
16
+ @content = content
17
+ @image_url = image_url
18
+ end
19
+
20
+ def to_s
21
+ content.to_s
22
+ end
23
+ end
24
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Langchain
4
- VERSION = "0.19.3"
4
+ VERSION = "0.19.4"
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: langchainrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.3
4
+ version: 0.19.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Bondarev
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-01-13 00:00:00.000000000 Z
10
+ date: 2025-02-17 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: baran
@@ -718,6 +717,7 @@ files:
718
717
  - lib/langchain/loader.rb
719
718
  - lib/langchain/output_parsers/base.rb
720
719
  - lib/langchain/output_parsers/output_fixing_parser.rb
720
+ - lib/langchain/output_parsers/output_parser_exception.rb
721
721
  - lib/langchain/output_parsers/prompts/naive_fix_prompt.yaml
722
722
  - lib/langchain/output_parsers/structured_output_parser.rb
723
723
  - lib/langchain/processors/base.rb
@@ -749,6 +749,7 @@ files:
749
749
  - lib/langchain/tool/weather.rb
750
750
  - lib/langchain/tool/wikipedia.rb
751
751
  - lib/langchain/tool_definition.rb
752
+ - lib/langchain/tool_response.rb
752
753
  - lib/langchain/utils/cosine_similarity.rb
753
754
  - lib/langchain/utils/hash_transformer.rb
754
755
  - lib/langchain/utils/image_wrapper.rb
@@ -775,7 +776,6 @@ metadata:
775
776
  source_code_uri: https://github.com/patterns-ai-core/langchainrb
776
777
  changelog_uri: https://github.com/patterns-ai-core/langchainrb/blob/main/CHANGELOG.md
777
778
  documentation_uri: https://rubydoc.info/gems/langchainrb
778
- post_install_message:
779
779
  rdoc_options: []
780
780
  require_paths:
781
781
  - lib
@@ -790,8 +790,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
790
790
  - !ruby/object:Gem::Version
791
791
  version: '0'
792
792
  requirements: []
793
- rubygems_version: 3.5.3
794
- signing_key:
793
+ rubygems_version: 3.6.2
795
794
  specification_version: 4
796
795
  summary: Build LLM-backed Ruby applications with Ruby's Langchain.rb
797
796
  test_files: []