raix 0.9.0 → 0.9.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed80bdf079ec738beb42df145184b5b23b5ad6c2f507233afd9aed47a1ac61f9
4
- data.tar.gz: b38006ad88398d9584d29fad763b7361cd22f7d6d6fb3d51e70b6ac80a5c39aa
3
+ metadata.gz: 5464c46a401957877b024ec9a1140b6f2624add3f21c172253663988c74baa49
4
+ data.tar.gz: a556464db7a06866cf6ecd27530b7ddcbbc372fa8e4f96f7ba84f0c6467de3b1
5
5
  SHA512:
6
- metadata.gz: c896ee3e447b7d45e009cc0c18b6d5527f4fc2620773a27a94743c591e9f535be0f1692e4e4d2b1a60b2059b813e66c5c5c32464f7ea194a95d6b31e553fad59
7
- data.tar.gz: 822e63a7375693fec475f6056db092c5e1a75398cd513e7650b8bfeec6535fe42c85681b98d4867869bca7e83a156cae5ac8101573c6ebd25f899d86c9b1fe46
6
+ metadata.gz: c99d540f8f8c7c0c35a57628d322ff0eef31803c0bee7244476633bd092320bd7040ceebb4b20126b55529bfe0fb373fee01e4977871a3eedf1e135331e9b4e2
7
+ data.tar.gz: 53e124ebdf99b0b176fb0c30d15c40e9a3156e210686d705237aa43fc02e2ca0e71a63e5efd75deffaf1f5565a34837ccd6d7f1b1dd6f2cbf6b6d9d5c3f137d2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## [0.9.2] - 2025-06-03
2
+ ### Fixed
3
+ - Fixed OpenAI chat completion compatibility
4
+ - Fixed SHA256 hexdigest generation for MCP tool names
5
+ - Added ostruct as explicit dependency to prevent warnings
6
+ - Fixed rubocop lint error for alphabetized gemspec dependencies
7
+ - Updated default OpenRouter model
8
+
9
+ ## [0.9.1] - 2025-05-30
10
+ ### Added
11
+ - **MCP Type Coercion** - Automatic type conversion for MCP tool arguments based on JSON schema
12
+ - Supports integer, number, boolean, array, and object types
13
+ - Handles nested objects and arrays of objects with proper coercion
14
+ - Gracefully handles invalid JSON and type mismatches
15
+ - **MCP Image Support** - MCP tools can now return image content as structured JSON
16
+
17
+ ### Fixed
18
+ - Fixed handling of nil values in MCP argument coercion
19
+
1
20
  ## [0.9.0] - 2025-05-30
2
21
  ### Added
3
22
  - **MCP (Model Context Protocol) Support**
data/Gemfile.lock CHANGED
@@ -1,10 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- raix (0.9.0)
4
+ raix (0.9.2)
5
5
  activesupport (>= 6.0)
6
6
  faraday-retry (~> 2.0)
7
7
  open_router (~> 0.2)
8
+ ostruct
8
9
  ruby-openai (~> 7)
9
10
 
10
11
  GEM
@@ -97,6 +98,7 @@ GEM
97
98
  dotenv (>= 2)
98
99
  faraday (>= 1)
99
100
  faraday-multipart (>= 1)
101
+ ostruct (0.6.1)
100
102
  parallel (1.24.0)
101
103
  parser (3.3.0.5)
102
104
  ast (~> 2.4.1)
@@ -3,6 +3,7 @@ require "json"
3
3
  require "securerandom"
4
4
  require "faraday"
5
5
  require "uri"
6
+ require "digest"
6
7
 
7
8
  module Raix
8
9
  module MCP
@@ -42,7 +43,8 @@ module Raix
42
43
  end
43
44
  end
44
45
 
45
- # Executes a tool with given arguments, returns text content.
46
+ # Executes a tool with given arguments.
47
+ # Returns text content directly, or JSON-encoded data for other content types.
46
48
  def call_tool(name, **arguments)
47
49
  request_id = SecureRandom.uuid
48
50
  send_json_rpc(request_id, "tools/call", name:, arguments:)
@@ -56,9 +58,18 @@ module Raix
56
58
  first_item = content.first
57
59
  case first_item
58
60
  when Hash
59
- if first_item[:type] == "text"
61
+ case first_item[:type]
62
+ when "text"
60
63
  first_item[:text]
64
+ when "image"
65
+ # Return a structured response for images
66
+ {
67
+ type: "image",
68
+ data: first_item[:data],
69
+ mime_type: first_item[:mimeType] || "image/png"
70
+ }.to_json
61
71
  else
72
+ # For any other type, return the item as JSON
62
73
  first_item.to_json
63
74
  end
64
75
  else
@@ -74,7 +85,8 @@ module Raix
74
85
  end
75
86
 
76
87
  def unique_key
77
- @url.parameterize.underscore.gsub("https_", "")
88
+ parametrized_url = @url.parameterize.underscore.gsub("https_", "")
89
+ Digest::SHA256.hexdigest(parametrized_url)[0..2]
78
90
  end
79
91
 
80
92
  private
@@ -1,6 +1,7 @@
1
1
  require_relative "tool"
2
2
  require "json"
3
3
  require "securerandom"
4
+ require "digest"
4
5
 
5
6
  module Raix
6
7
  module MCP
@@ -21,14 +22,34 @@ module Raix
21
22
  end
22
23
  end
23
24
 
24
- # Executes a tool with given arguments, returns text content.
25
+ # Executes a tool with given arguments.
26
+ # Returns text content directly, or JSON-encoded data for other content types.
25
27
  def call_tool(name, **arguments)
26
28
  result = call("tools/call", name:, arguments:)
27
- unless result.dig("content", 0, "type") == "text"
28
- raise NotImplementedError, "Only text is supported"
29
- end
29
+ content = result["content"]
30
+ return "" if content.nil? || content.empty?
30
31
 
31
- result.dig("content", 0, "text")
32
+ # Handle different content formats
33
+ first_item = content.first
34
+ case first_item
35
+ when Hash
36
+ case first_item["type"]
37
+ when "text"
38
+ first_item["text"]
39
+ when "image"
40
+ # Return a structured response for images
41
+ {
42
+ type: "image",
43
+ data: first_item["data"],
44
+ mime_type: first_item["mimeType"] || "image/png"
45
+ }.to_json
46
+ else
47
+ # For any other type, return the item as JSON
48
+ first_item.to_json
49
+ end
50
+ else
51
+ first_item.to_s
52
+ end
32
53
  end
33
54
 
34
55
  # Closes the connection to the server.
@@ -37,7 +58,8 @@ module Raix
37
58
  end
38
59
 
39
60
  def unique_key
40
- @args.join(" ").parameterize.underscore
61
+ parametrized_args = @args.join(" ").parameterize.underscore
62
+ Digest::SHA256.hexdigest(parametrized_args)[0..2]
41
63
  end
42
64
 
43
65
  private
@@ -60,12 +60,12 @@ module Raix
60
60
  # @param params [Hash] The parameters for chat completion.
61
61
  # @option loop [Boolean] :loop (false) Whether to loop the chat completion after function calls.
62
62
  # @option params [Boolean] :json (false) Whether to return the parse the response as a JSON object. Will search for <json> tags in the response first, then fall back to the default JSON parsing of the entire response.
63
- # @option params [Boolean] :openai (false) Whether to use OpenAI's API instead of OpenRouter's.
63
+ # @option params [String] :openai (nil) If non-nil, use OpenAI with the model specified in this param.
64
64
  # @option params [Boolean] :raw (false) Whether to return the raw response or dig the text content.
65
65
  # @option params [Array] :messages (nil) An array of messages to use instead of the transcript.
66
66
  # @option tools [Array|false] :available_tools (nil) Tools to pass to the LLM. Ignored if nil (default). If false, no tools are passed. If an array, only declared tools in the array are passed.
67
67
  # @return [String|Hash] The completed chat response.
68
- def chat_completion(params: {}, loop: false, json: false, raw: false, openai: false, save_response: true, messages: nil, available_tools: nil)
68
+ def chat_completion(params: {}, loop: false, json: false, raw: false, openai: nil, save_response: true, messages: nil, available_tools: nil)
69
69
  # set params to default values if not provided
70
70
  params[:cache_at] ||= cache_at.presence
71
71
  params[:frequency_penalty] ||= frequency_penalty.presence
@@ -38,7 +38,7 @@ module Raix
38
38
 
39
39
  DEFAULT_MAX_TOKENS = 1000
40
40
  DEFAULT_MAX_COMPLETION_TOKENS = 16_384
41
- DEFAULT_MODEL = "meta-llama/llama-3-8b-instruct:free"
41
+ DEFAULT_MODEL = "meta-llama/llama-3.3-8b-instruct:free"
42
42
  DEFAULT_TEMPERATURE = 0.0
43
43
 
44
44
  # Initializes a new instance of the Configuration class with default values.
data/lib/raix/mcp.rb CHANGED
@@ -102,7 +102,7 @@ module Raix
102
102
  filtered_tools.each do |tool|
103
103
  remote_name = tool.name
104
104
  # TODO: Revisit later whether this much context is needed in the function name
105
- local_name = "#{client.unique_key}_#{remote_name}".to_sym
105
+ local_name = "#{remote_name}_#{client.unique_key}".to_sym
106
106
 
107
107
  description = tool.description
108
108
  input_schema = tool.input_schema || {}
@@ -115,11 +115,19 @@ module Raix
115
115
  # Required by OpenAI
116
116
  latest_definition[:parameters][:properties] ||= {}
117
117
 
118
+ # Store the schema for type coercion
119
+ tool_schemas = @tool_schemas ||= {}
120
+ tool_schemas[local_name] = input_schema
121
+
118
122
  # --- define an instance method that proxies to the server
119
123
  define_method(local_name) do |arguments, _cache|
120
124
  arguments ||= {}
121
125
 
122
- content_text = client.call_tool(remote_name, **arguments)
126
+ # Coerce argument types based on the input schema
127
+ stored_schema = self.class.instance_variable_get(:@tool_schemas)&.dig(local_name)
128
+ coerced_arguments = coerce_arguments(arguments, stored_schema)
129
+
130
+ content_text = client.call_tool(remote_name, **coerced_arguments)
123
131
  call_id = SecureRandom.uuid
124
132
 
125
133
  # Mirror FunctionDispatch transcript behaviour
@@ -132,7 +140,7 @@ module Raix
132
140
  id: call_id,
133
141
  type: "function",
134
142
  function: {
135
- name: remote_name,
143
+ name: local_name.to_s,
136
144
  arguments: arguments.to_json
137
145
  }
138
146
  }
@@ -141,7 +149,7 @@ module Raix
141
149
  {
142
150
  role: "tool",
143
151
  tool_call_id: call_id,
144
- name: remote_name,
152
+ name: local_name.to_s,
145
153
  content: content_text
146
154
  }
147
155
  ]
@@ -157,5 +165,92 @@ module Raix
157
165
  @mcp_servers[client.unique_key] = { tools: filtered_tools, client: }
158
166
  end
159
167
  end
168
+
169
+ private
170
+
171
+ # Coerce argument types based on the JSON schema
172
+ def coerce_arguments(arguments, schema)
173
+ return arguments unless schema.is_a?(Hash) && schema["properties"].is_a?(Hash)
174
+
175
+ coerced = {}
176
+ schema["properties"].each do |key, prop_schema|
177
+ value = if arguments.key?(key)
178
+ arguments[key]
179
+ elsif arguments.key?(key.to_sym)
180
+ arguments[key.to_sym]
181
+ end
182
+ next if value.nil?
183
+
184
+ coerced[key] = coerce_value(value, prop_schema)
185
+ end
186
+
187
+ # Include any additional arguments not in the schema
188
+ arguments.each do |key, value|
189
+ key_str = key.to_s
190
+ coerced[key_str] = value unless coerced.key?(key_str)
191
+ end
192
+
193
+ coerced.with_indifferent_access
194
+ end
195
+
196
+ # Coerce a single value based on its schema
197
+ def coerce_value(value, schema)
198
+ return value unless schema.is_a?(Hash)
199
+
200
+ case schema["type"]
201
+ when "number", "integer"
202
+ if value.is_a?(String) && value.match?(/\A-?\d+(\.\d+)?\z/)
203
+ schema["type"] == "integer" ? value.to_i : value.to_f
204
+ else
205
+ value
206
+ end
207
+ when "boolean"
208
+ case value
209
+ when "true", true then true
210
+ when "false", false then false
211
+ else value
212
+ end
213
+ when "array"
214
+ array_value = begin
215
+ value.is_a?(String) ? JSON.parse(value) : value
216
+ rescue JSON::ParserError
217
+ value
218
+ end
219
+
220
+ # If there's an items schema, coerce each element
221
+ if array_value.is_a?(Array) && schema["items"]
222
+ array_value.map { |item| coerce_value(item, schema["items"]) }
223
+ else
224
+ array_value
225
+ end
226
+ when "object"
227
+ object_value = begin
228
+ value.is_a?(String) ? JSON.parse(value) : value
229
+ rescue JSON::ParserError
230
+ value
231
+ end
232
+
233
+ # If there are properties defined, coerce them recursively
234
+ if object_value.is_a?(Hash) && schema["properties"]
235
+ coerced_object = {}
236
+ schema["properties"].each do |prop_key, prop_schema|
237
+ prop_value = object_value[prop_key] || object_value[prop_key.to_sym]
238
+ coerced_object[prop_key] = coerce_value(prop_value, prop_schema) unless prop_value.nil?
239
+ end
240
+
241
+ # Include any additional properties not in the schema
242
+ object_value.each do |obj_key, obj_value|
243
+ obj_key_str = obj_key.to_s
244
+ coerced_object[obj_key_str] = obj_value unless coerced_object.key?(obj_key_str)
245
+ end
246
+
247
+ coerced_object
248
+ else
249
+ object_value
250
+ end
251
+ else
252
+ value
253
+ end
254
+ end
160
255
  end
161
256
  end
data/lib/raix/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Raix
4
- VERSION = "0.9.0"
4
+ VERSION = "0.9.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: raix
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Obie Fernandez
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0.2'
54
+ - !ruby/object:Gem::Dependency
55
+ name: ostruct
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
54
68
  - !ruby/object:Gem::Dependency
55
69
  name: ruby-openai
56
70
  requirement: !ruby/object:Gem::Requirement
@@ -96,7 +110,6 @@ files:
96
110
  - lib/raix/prompt_declarations.rb
97
111
  - lib/raix/response_format.rb
98
112
  - lib/raix/version.rb
99
- - raix.gemspec
100
113
  - sig/raix.rbs
101
114
  homepage: https://github.com/OlympiaAI/raix
102
115
  licenses:
data/raix.gemspec DELETED
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/raix/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "raix"
7
- spec.version = Raix::VERSION
8
- spec.authors = ["Obie Fernandez"]
9
- spec.email = ["obiefernandez@gmail.com"]
10
-
11
- spec.summary = "Ruby AI eXtensions"
12
- spec.homepage = "https://github.com/OlympiaAI/raix"
13
- spec.license = "MIT"
14
- spec.required_ruby_version = ">= 3.2.2"
15
-
16
- spec.metadata["homepage_uri"] = spec.homepage
17
- spec.metadata["source_code_uri"] = "https://github.com/OlympiaAI/raix"
18
- spec.metadata["changelog_uri"] = "https://github.com/OlympiaAI/raix/blob/main/CHANGELOG.md"
19
-
20
- # Specify which files should be added to the gem when it is released.
21
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
- spec.files = Dir.chdir(__dir__) do
23
- `git ls-files -z`.split("\x0").reject do |f|
24
- (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
25
- end
26
- end
27
- spec.bindir = "exe"
28
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
- spec.require_paths = ["lib"]
30
-
31
- spec.add_dependency "activesupport", ">= 6.0"
32
- spec.add_dependency "faraday-retry", "~> 2.0"
33
- spec.add_dependency "open_router", "~> 0.2"
34
- spec.add_dependency "ruby-openai", "~> 7"
35
- end