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 +4 -4
- data/CHANGELOG.md +19 -0
- data/Gemfile.lock +3 -1
- data/lib/mcp/sse_client.rb +15 -3
- data/lib/mcp/stdio_client.rb +28 -6
- data/lib/raix/chat_completion.rb +2 -2
- data/lib/raix/configuration.rb +1 -1
- data/lib/raix/mcp.rb +99 -4
- data/lib/raix/version.rb +1 -1
- metadata +15 -2
- data/raix.gemspec +0 -35
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5464c46a401957877b024ec9a1140b6f2624add3f21c172253663988c74baa49
|
4
|
+
data.tar.gz: a556464db7a06866cf6ecd27530b7ddcbbc372fa8e4f96f7ba84f0c6467de3b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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)
|
data/lib/mcp/sse_client.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
data/lib/mcp/stdio_client.rb
CHANGED
@@ -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
|
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
|
-
|
28
|
-
|
29
|
-
end
|
29
|
+
content = result["content"]
|
30
|
+
return "" if content.nil? || content.empty?
|
30
31
|
|
31
|
-
|
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
|
data/lib/raix/chat_completion.rb
CHANGED
@@ -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 [
|
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:
|
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
|
data/lib/raix/configuration.rb
CHANGED
@@ -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 = "#{
|
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
|
-
|
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:
|
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:
|
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
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.
|
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
|