raix 0.8.6 → 0.9.1
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/.rubocop.yml +3 -0
- data/CHANGELOG.md +26 -0
- data/Gemfile.lock +1 -1
- data/README.md +15 -0
- data/lib/mcp/sse_client.rb +297 -0
- data/lib/mcp/stdio_client.rb +78 -0
- data/lib/mcp/tool.rb +67 -0
- data/lib/raix/chat_completion.rb +23 -6
- data/lib/raix/configuration.rb +65 -0
- data/lib/raix/mcp.rb +130 -211
- data/lib/raix/predicate.rb +1 -4
- data/lib/raix/version.rb +1 -1
- data/lib/raix.rb +1 -36
- metadata +5 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0868c4163ea58511e3c0dfc66b2b22a5cb2dbb16a7575746b36cd70af4777137'
|
4
|
+
data.tar.gz: 4e396262603a787dc58817e9fbb536264f25cdb9462bcbe035d675e59cb179ad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ae8a80e0e45dfba22290aefe27c39fd914f9046228dac33af1bc631e55a9eff73e38500c12c9eec6ba7bff67a5a16d51438ccf80cbcf423b070125dcd5710c4
|
7
|
+
data.tar.gz: ffb45352168291e28041b787fa8c2bf61c032d5bd5c59fb5a5653a602d6b1bb5ef282475ce7d828722113df797369127a1a4cdeec5da1c53ad887f1665b0385a
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,29 @@
|
|
1
|
+
## [0.9.1] - 2025-05-30
|
2
|
+
### Added
|
3
|
+
- **MCP Type Coercion** - Automatic type conversion for MCP tool arguments based on JSON schema
|
4
|
+
- Supports integer, number, boolean, array, and object types
|
5
|
+
- Handles nested objects and arrays of objects with proper coercion
|
6
|
+
- Gracefully handles invalid JSON and type mismatches
|
7
|
+
- **MCP Image Support** - MCP tools can now return image content as structured JSON
|
8
|
+
|
9
|
+
### Fixed
|
10
|
+
- Fixed handling of nil values in MCP argument coercion
|
11
|
+
|
12
|
+
## [0.9.0] - 2025-05-30
|
13
|
+
### Added
|
14
|
+
- **MCP (Model Context Protocol) Support**
|
15
|
+
- New `stdio_mcp` method for stdio-based MCP servers
|
16
|
+
- Refactored existing MCP code into `SseClient` and `StdioClient`
|
17
|
+
- Split top-level `mcp` method into `sse_mcp` and `stdio_mcp`
|
18
|
+
- Added authentication support for MCP servers
|
19
|
+
- **Class-Level Configuration**
|
20
|
+
- Moved configuration to separate `Configuration` class
|
21
|
+
- Added fallback mechanism for configuration options
|
22
|
+
- Cleaner metaprogramming implementation
|
23
|
+
|
24
|
+
### Fixed
|
25
|
+
- Fixed method signature of functions added via MCP
|
26
|
+
|
1
27
|
## [0.8.6] - 2025-05-19
|
2
28
|
- add `required` and `optional` flags for parameters in `function` declarations
|
3
29
|
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -703,6 +703,21 @@ You can add an initializer to your application's `config/initializers` directory
|
|
703
703
|
|
704
704
|
You will also need to configure the OpenRouter API access token as per the instructions here: https://github.com/OlympiaAI/open_router?tab=readme-ov-file#quickstart
|
705
705
|
|
706
|
+
### Global vs class level configuration
|
707
|
+
|
708
|
+
You can either configure Raix globally or at the class level. The global configuration is set in the initializer as shown above. You can however also override all configuration options of the `Configuration` class on the class level with the
|
709
|
+
same syntax:
|
710
|
+
|
711
|
+
```ruby
|
712
|
+
class MyClass
|
713
|
+
include Raix::ChatCompletion
|
714
|
+
|
715
|
+
configure do |config|
|
716
|
+
config.openrouter_client = OpenRouter::Client.new # with my special options
|
717
|
+
end
|
718
|
+
end
|
719
|
+
```
|
720
|
+
|
706
721
|
## Development
|
707
722
|
|
708
723
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -0,0 +1,297 @@
|
|
1
|
+
require_relative "tool"
|
2
|
+
require "json"
|
3
|
+
require "securerandom"
|
4
|
+
require "faraday"
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
module Raix
|
8
|
+
module MCP
|
9
|
+
# Client for communicating with MCP servers via Server-Sent Events (SSE).
|
10
|
+
class SseClient
|
11
|
+
PROTOCOL_VERSION = "2024-11-05".freeze
|
12
|
+
CONNECTION_TIMEOUT = 10
|
13
|
+
OPEN_TIMEOUT = 30
|
14
|
+
|
15
|
+
# Creates a new client and establishes SSE connection to discover the JSON-RPC endpoint.
|
16
|
+
#
|
17
|
+
# @param url [String] the SSE endpoint URL
|
18
|
+
def initialize(url, headers: {})
|
19
|
+
@url = url
|
20
|
+
@endpoint_url = nil
|
21
|
+
@sse_thread = nil
|
22
|
+
@event_queue = Thread::Queue.new
|
23
|
+
@buffer = ""
|
24
|
+
@closed = false
|
25
|
+
@headers = headers
|
26
|
+
|
27
|
+
# Start the SSE connection and discover endpoint
|
28
|
+
establish_sse_connection
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns available tools from the server.
|
32
|
+
def tools
|
33
|
+
@tools ||= begin
|
34
|
+
request_id = SecureRandom.uuid
|
35
|
+
send_json_rpc(request_id, "tools/list", {})
|
36
|
+
|
37
|
+
# Wait for response through SSE
|
38
|
+
response = wait_for_response(request_id)
|
39
|
+
response[:tools].map do |tool_json|
|
40
|
+
Tool.from_json(tool_json)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Executes a tool with given arguments.
|
46
|
+
# Returns text content directly, or JSON-encoded data for other content types.
|
47
|
+
def call_tool(name, **arguments)
|
48
|
+
request_id = SecureRandom.uuid
|
49
|
+
send_json_rpc(request_id, "tools/call", name:, arguments:)
|
50
|
+
|
51
|
+
# Wait for response through SSE
|
52
|
+
response = wait_for_response(request_id)
|
53
|
+
content = response[:content]
|
54
|
+
return "" if content.nil? || content.empty?
|
55
|
+
|
56
|
+
# Handle different content formats
|
57
|
+
first_item = content.first
|
58
|
+
case first_item
|
59
|
+
when Hash
|
60
|
+
case first_item[:type]
|
61
|
+
when "text"
|
62
|
+
first_item[:text]
|
63
|
+
when "image"
|
64
|
+
# Return a structured response for images
|
65
|
+
{
|
66
|
+
type: "image",
|
67
|
+
data: first_item[:data],
|
68
|
+
mime_type: first_item[:mimeType] || "image/png"
|
69
|
+
}.to_json
|
70
|
+
else
|
71
|
+
# For any other type, return the item as JSON
|
72
|
+
first_item.to_json
|
73
|
+
end
|
74
|
+
else
|
75
|
+
first_item.to_s
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Closes the connection to the server.
|
80
|
+
def close
|
81
|
+
@closed = true
|
82
|
+
@sse_thread&.kill
|
83
|
+
@connection&.close
|
84
|
+
end
|
85
|
+
|
86
|
+
def unique_key
|
87
|
+
@url.parameterize.underscore.gsub("https_", "")
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Establishes and maintains the SSE connection
|
93
|
+
def establish_sse_connection
|
94
|
+
@sse_thread = Thread.new do
|
95
|
+
headers = {
|
96
|
+
"Accept" => "text/event-stream",
|
97
|
+
"Cache-Control" => "no-cache",
|
98
|
+
"Connection" => "keep-alive",
|
99
|
+
"MCP-Version" => PROTOCOL_VERSION
|
100
|
+
}.merge(@headers)
|
101
|
+
|
102
|
+
@connection = Faraday.new(url: @url) do |faraday|
|
103
|
+
faraday.options.timeout = CONNECTION_TIMEOUT
|
104
|
+
faraday.options.open_timeout = OPEN_TIMEOUT
|
105
|
+
end
|
106
|
+
|
107
|
+
@connection.get do |req|
|
108
|
+
req.headers = headers
|
109
|
+
req.options.on_data = proc do |chunk, _size|
|
110
|
+
next if @closed
|
111
|
+
|
112
|
+
@buffer << chunk
|
113
|
+
process_sse_buffer
|
114
|
+
end
|
115
|
+
end
|
116
|
+
rescue StandardError => e
|
117
|
+
# puts "[MCP DEBUG] SSE connection error: #{e.message}"
|
118
|
+
@event_queue << { error: e }
|
119
|
+
end
|
120
|
+
|
121
|
+
# Wait for endpoint discovery
|
122
|
+
loop do
|
123
|
+
event = @event_queue.pop
|
124
|
+
if event[:error]
|
125
|
+
raise ProtocolError, "SSE connection failed: #{event[:error].message}"
|
126
|
+
elsif event[:endpoint_url]
|
127
|
+
@endpoint_url = event[:endpoint_url]
|
128
|
+
break
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Initialize the MCP session
|
133
|
+
initialize_mcp_session
|
134
|
+
end
|
135
|
+
|
136
|
+
# Process SSE buffer for complete events
|
137
|
+
def process_sse_buffer
|
138
|
+
while (idx = @buffer.index("\n\n"))
|
139
|
+
event_text = @buffer.slice!(0..idx + 1)
|
140
|
+
event_type, event_data = parse_sse_fields(event_text)
|
141
|
+
|
142
|
+
case event_type
|
143
|
+
when "endpoint"
|
144
|
+
endpoint_url = build_absolute_url(@url, event_data)
|
145
|
+
@event_queue << { endpoint_url: }
|
146
|
+
when "message"
|
147
|
+
handle_message_event(event_data)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Handle SSE message events
|
153
|
+
def handle_message_event(event_data)
|
154
|
+
parsed = JSON.parse(event_data, symbolize_names: true)
|
155
|
+
|
156
|
+
# Handle different message types
|
157
|
+
case parsed
|
158
|
+
when ->(p) { p[:method] == "initialize" && p.dig(:params, :endpoint_url) }
|
159
|
+
# Legacy endpoint discovery
|
160
|
+
endpoint_url = parsed.dig(:params, :endpoint_url)
|
161
|
+
@event_queue << { endpoint_url: }
|
162
|
+
when ->(p) { p[:id] && p[:result] }
|
163
|
+
@event_queue << { id: parsed[:id], result: parsed[:result] }
|
164
|
+
when ->(p) { p[:result] }
|
165
|
+
@event_queue << { result: parsed[:result] }
|
166
|
+
end
|
167
|
+
rescue JSON::ParserError => e
|
168
|
+
puts "[MCP DEBUG] Error parsing message: #{e.message}"
|
169
|
+
puts "[MCP DEBUG] Message data: #{event_data}"
|
170
|
+
end
|
171
|
+
|
172
|
+
# Initialize the MCP session
|
173
|
+
def initialize_mcp_session
|
174
|
+
request_id = SecureRandom.uuid
|
175
|
+
send_json_rpc(request_id, "initialize", {
|
176
|
+
protocolVersion: PROTOCOL_VERSION,
|
177
|
+
capabilities: {
|
178
|
+
roots: { listChanged: true },
|
179
|
+
sampling: {}
|
180
|
+
},
|
181
|
+
clientInfo: {
|
182
|
+
name: "Raix",
|
183
|
+
version: Raix::VERSION
|
184
|
+
}
|
185
|
+
})
|
186
|
+
|
187
|
+
# Wait for initialization response
|
188
|
+
response = wait_for_response(request_id)
|
189
|
+
|
190
|
+
# Send acknowledgment if needed
|
191
|
+
return unless response.dig(:capabilities, :tools, :listChanged)
|
192
|
+
|
193
|
+
send_notification("notifications/initialized", {})
|
194
|
+
end
|
195
|
+
|
196
|
+
# Send a JSON-RPC request
|
197
|
+
def send_json_rpc(id, method, params)
|
198
|
+
body = {
|
199
|
+
jsonrpc: JSONRPC_VERSION,
|
200
|
+
id:,
|
201
|
+
method:,
|
202
|
+
params:
|
203
|
+
}
|
204
|
+
|
205
|
+
# Use a new connection for the POST request
|
206
|
+
conn = Faraday.new(url: @endpoint_url) do |faraday|
|
207
|
+
faraday.options.timeout = CONNECTION_TIMEOUT
|
208
|
+
end
|
209
|
+
|
210
|
+
conn.post do |req|
|
211
|
+
req.headers["Content-Type"] = "application/json"
|
212
|
+
req.body = body.to_json
|
213
|
+
end
|
214
|
+
rescue StandardError => e
|
215
|
+
raise ProtocolError, "Failed to send request: #{e.message}"
|
216
|
+
end
|
217
|
+
|
218
|
+
# Send a notification (no response expected)
|
219
|
+
def send_notification(method, params)
|
220
|
+
body = {
|
221
|
+
jsonrpc: JSONRPC_VERSION,
|
222
|
+
method:,
|
223
|
+
params:
|
224
|
+
}
|
225
|
+
|
226
|
+
conn = Faraday.new(url: @endpoint_url) do |faraday|
|
227
|
+
faraday.options.timeout = CONNECTION_TIMEOUT
|
228
|
+
end
|
229
|
+
|
230
|
+
conn.post do |req|
|
231
|
+
req.headers["Content-Type"] = "application/json"
|
232
|
+
req.body = body.to_json
|
233
|
+
end
|
234
|
+
rescue StandardError => e
|
235
|
+
puts "[MCP DEBUG] Error sending notification: #{e.message}"
|
236
|
+
end
|
237
|
+
|
238
|
+
# Wait for a response with a specific ID
|
239
|
+
def wait_for_response(request_id)
|
240
|
+
timeout = Time.now + CONNECTION_TIMEOUT
|
241
|
+
|
242
|
+
loop do
|
243
|
+
if Time.now > timeout
|
244
|
+
raise ProtocolError, "Timeout waiting for response"
|
245
|
+
end
|
246
|
+
|
247
|
+
# Use non-blocking pop with timeout
|
248
|
+
begin
|
249
|
+
event = @event_queue.pop(true) # non_block = true
|
250
|
+
rescue ThreadError
|
251
|
+
# Queue is empty, wait a bit
|
252
|
+
sleep 0.1
|
253
|
+
next
|
254
|
+
end
|
255
|
+
|
256
|
+
if event[:error]
|
257
|
+
raise ProtocolError, "SSE error: #{event[:error].message}"
|
258
|
+
elsif event[:id] == request_id && event[:result]
|
259
|
+
return event[:result]
|
260
|
+
elsif event[:result] && !event[:id]
|
261
|
+
return event[:result]
|
262
|
+
else
|
263
|
+
@event_queue << event
|
264
|
+
sleep 0.01
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# Parses SSE event fields from raw text.
|
270
|
+
def parse_sse_fields(event_text)
|
271
|
+
event_type = "message"
|
272
|
+
data_lines = []
|
273
|
+
|
274
|
+
event_text.each_line do |line|
|
275
|
+
case line
|
276
|
+
when /^event:\s*(.+)$/
|
277
|
+
event_type = Regexp.last_match(1).strip
|
278
|
+
when /^data:\s*(.*)$/
|
279
|
+
data_lines << Regexp.last_match(1)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
[event_type, data_lines.join("\n").strip]
|
284
|
+
end
|
285
|
+
|
286
|
+
# Builds an absolute URL for candidate relative to base.
|
287
|
+
def build_absolute_url(base, candidate)
|
288
|
+
uri = URI.parse(candidate)
|
289
|
+
return candidate if uri.absolute?
|
290
|
+
|
291
|
+
URI.join(base, candidate).to_s
|
292
|
+
rescue URI::InvalidURIError
|
293
|
+
candidate
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require_relative "tool"
|
2
|
+
require "json"
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
module Raix
|
6
|
+
module MCP
|
7
|
+
# Client for communicating with MCP servers via stdio using JSON-RPC.
|
8
|
+
class StdioClient
|
9
|
+
# Creates a new client with a bidirectional pipe to the MCP server.
|
10
|
+
def initialize(*args, env)
|
11
|
+
@args = args
|
12
|
+
@io = IO.popen(env, args, "w+")
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns available tools from the server.
|
16
|
+
def tools
|
17
|
+
result = call("tools/list")
|
18
|
+
|
19
|
+
result["tools"].map do |tool_json|
|
20
|
+
Tool.from_json(tool_json)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Executes a tool with given arguments.
|
25
|
+
# Returns text content directly, or JSON-encoded data for other content types.
|
26
|
+
def call_tool(name, **arguments)
|
27
|
+
result = call("tools/call", name:, arguments:)
|
28
|
+
content = result["content"]
|
29
|
+
return "" if content.nil? || content.empty?
|
30
|
+
|
31
|
+
# Handle different content formats
|
32
|
+
first_item = content.first
|
33
|
+
case first_item
|
34
|
+
when Hash
|
35
|
+
case first_item["type"]
|
36
|
+
when "text"
|
37
|
+
first_item["text"]
|
38
|
+
when "image"
|
39
|
+
# Return a structured response for images
|
40
|
+
{
|
41
|
+
type: "image",
|
42
|
+
data: first_item["data"],
|
43
|
+
mime_type: first_item["mimeType"] || "image/png"
|
44
|
+
}.to_json
|
45
|
+
else
|
46
|
+
# For any other type, return the item as JSON
|
47
|
+
first_item.to_json
|
48
|
+
end
|
49
|
+
else
|
50
|
+
first_item.to_s
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Closes the connection to the server.
|
55
|
+
def close
|
56
|
+
@io.close
|
57
|
+
end
|
58
|
+
|
59
|
+
def unique_key
|
60
|
+
@args.join(" ").parameterize.underscore
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
# Sends JSON-RPC request and returns the result.
|
66
|
+
def call(method, **params)
|
67
|
+
@io.puts({ id: SecureRandom.uuid, method:, params:, jsonrpc: JSONRPC_VERSION }.to_json)
|
68
|
+
@io.flush # Ensure data is immediately sent
|
69
|
+
message = JSON.parse(@io.gets)
|
70
|
+
if (error = message["error"])
|
71
|
+
raise ProtocolError, error["message"]
|
72
|
+
end
|
73
|
+
|
74
|
+
message["result"]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/mcp/tool.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
module Raix
|
2
|
+
module MCP
|
3
|
+
# Represents an MCP (Model Context Protocol) tool with metadata and schema
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# tool = Tool.new(
|
7
|
+
# name: "weather",
|
8
|
+
# description: "Get weather info",
|
9
|
+
# input_schema: { "type" => "object", "properties" => { "city" => { "type" => "string" } } }
|
10
|
+
# )
|
11
|
+
class Tool
|
12
|
+
attr_reader :name, :description, :input_schema
|
13
|
+
|
14
|
+
# Initialize a new Tool
|
15
|
+
#
|
16
|
+
# @param name [String] the tool name
|
17
|
+
# @param description [String] human-readable description of what the tool does
|
18
|
+
# @param input_schema [Hash] JSON schema defining the tool's input parameters
|
19
|
+
def initialize(name:, description:, input_schema: {})
|
20
|
+
@name = name
|
21
|
+
@description = description
|
22
|
+
@input_schema = input_schema
|
23
|
+
end
|
24
|
+
|
25
|
+
# Initialize from raw MCP JSON response
|
26
|
+
#
|
27
|
+
# @param json [Hash] parsed JSON data from MCP response
|
28
|
+
# @return [Tool] new Tool instance
|
29
|
+
def self.from_json(json)
|
30
|
+
new(
|
31
|
+
name: json[:name] || json["name"],
|
32
|
+
description: json[:description] || json["description"],
|
33
|
+
input_schema: json[:inputSchema] || json["inputSchema"] || {}
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Get the input schema type
|
38
|
+
#
|
39
|
+
# @return [String, nil] the schema type (e.g., "object")
|
40
|
+
def input_type
|
41
|
+
input_schema["type"]
|
42
|
+
end
|
43
|
+
|
44
|
+
# Get the properties hash
|
45
|
+
#
|
46
|
+
# @return [Hash] schema properties definition
|
47
|
+
def properties
|
48
|
+
input_schema["properties"] || {}
|
49
|
+
end
|
50
|
+
|
51
|
+
# Get required properties array
|
52
|
+
#
|
53
|
+
# @return [Array<String>] list of required property names
|
54
|
+
def required_properties
|
55
|
+
input_schema["required"] || []
|
56
|
+
end
|
57
|
+
|
58
|
+
# Check if a property is required
|
59
|
+
#
|
60
|
+
# @param property_name [String] name of the property to check
|
61
|
+
# @return [Boolean] true if the property is required
|
62
|
+
def required?(property_name)
|
63
|
+
required_properties.include?(property_name)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/raix/chat_completion.rb
CHANGED
@@ -38,6 +38,23 @@ module Raix
|
|
38
38
|
:prediction, :repetition_penalty, :response_format, :stream, :temperature, :max_completion_tokens,
|
39
39
|
:max_tokens, :seed, :stop, :top_a, :top_k, :top_logprobs, :top_p, :tools, :available_tools, :tool_choice, :provider
|
40
40
|
|
41
|
+
class_methods do
|
42
|
+
# Returns the current configuration of this class. Falls back to global configuration for unset values.
|
43
|
+
def configuration
|
44
|
+
@configuration ||= Configuration.new(fallback: Raix.configuration)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Let's you configure the class-level configuration using a block.
|
48
|
+
def configure
|
49
|
+
yield(configuration)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Instance level access to the class-level configuration.
|
54
|
+
def configuration
|
55
|
+
self.class.configuration
|
56
|
+
end
|
57
|
+
|
41
58
|
# This method performs chat completion based on the provided transcript and parameters.
|
42
59
|
#
|
43
60
|
# @param params [Hash] The parameters for chat completion.
|
@@ -54,8 +71,8 @@ module Raix
|
|
54
71
|
params[:frequency_penalty] ||= frequency_penalty.presence
|
55
72
|
params[:logit_bias] ||= logit_bias.presence
|
56
73
|
params[:logprobs] ||= logprobs.presence
|
57
|
-
params[:max_completion_tokens] ||= max_completion_tokens.presence ||
|
58
|
-
params[:max_tokens] ||= max_tokens.presence ||
|
74
|
+
params[:max_completion_tokens] ||= max_completion_tokens.presence || configuration.max_completion_tokens
|
75
|
+
params[:max_tokens] ||= max_tokens.presence || configuration.max_tokens
|
59
76
|
params[:min_p] ||= min_p.presence
|
60
77
|
params[:prediction] = { type: "content", content: params[:prediction] || prediction } if params[:prediction] || prediction.present?
|
61
78
|
params[:presence_penalty] ||= presence_penalty.presence
|
@@ -64,7 +81,7 @@ module Raix
|
|
64
81
|
params[:response_format] ||= response_format.presence
|
65
82
|
params[:seed] ||= seed.presence
|
66
83
|
params[:stop] ||= stop.presence
|
67
|
-
params[:temperature] ||= temperature.presence ||
|
84
|
+
params[:temperature] ||= temperature.presence || configuration.temperature
|
68
85
|
params[:tool_choice] ||= tool_choice.presence
|
69
86
|
params[:tools] = if available_tools == false
|
70
87
|
nil
|
@@ -95,7 +112,7 @@ module Raix
|
|
95
112
|
self.loop = loop
|
96
113
|
|
97
114
|
# set the model to the default if not provided
|
98
|
-
self.model ||=
|
115
|
+
self.model ||= configuration.model
|
99
116
|
|
100
117
|
adapter = MessageAdapters::Base.new(self)
|
101
118
|
|
@@ -227,7 +244,7 @@ module Raix
|
|
227
244
|
|
228
245
|
params.delete(:temperature) if model.start_with?("o")
|
229
246
|
|
230
|
-
|
247
|
+
configuration.openai_client.chat(parameters: params.compact.merge(model:, messages:))
|
231
248
|
end
|
232
249
|
|
233
250
|
def openrouter_request(params:, model:, messages:)
|
@@ -237,7 +254,7 @@ module Raix
|
|
237
254
|
retry_count = 0
|
238
255
|
|
239
256
|
begin
|
240
|
-
|
257
|
+
configuration.openrouter_client.complete(messages, model:, extras: params.compact, stream:)
|
241
258
|
rescue OpenRouter::ServerError => e
|
242
259
|
if e.message.include?("retry")
|
243
260
|
puts "Retrying OpenRouter request... (#{retry_count} attempts) #{e.message}"
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raix
|
4
|
+
# The Configuration class holds the configuration options for the Raix gem.
|
5
|
+
class Configuration
|
6
|
+
def self.attr_accessor_with_fallback(method_name)
|
7
|
+
define_method(method_name) do
|
8
|
+
value = instance_variable_get("@#{method_name}")
|
9
|
+
return value if value
|
10
|
+
return unless fallback
|
11
|
+
|
12
|
+
fallback.public_send(method_name)
|
13
|
+
end
|
14
|
+
define_method("#{method_name}=") do |value|
|
15
|
+
instance_variable_set("@#{method_name}", value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# The temperature option determines the randomness of the generated text.
|
20
|
+
# Higher values result in more random output.
|
21
|
+
attr_accessor_with_fallback :temperature
|
22
|
+
|
23
|
+
# The max_tokens option determines the maximum number of tokens to generate.
|
24
|
+
attr_accessor_with_fallback :max_tokens
|
25
|
+
|
26
|
+
# The max_completion_tokens option determines the maximum number of tokens to generate.
|
27
|
+
attr_accessor_with_fallback :max_completion_tokens
|
28
|
+
|
29
|
+
# The model option determines the model to use for text generation. This option
|
30
|
+
# is normally set in each class that includes the ChatCompletion module.
|
31
|
+
attr_accessor_with_fallback :model
|
32
|
+
|
33
|
+
# The openrouter_client option determines the default client to use for communication.
|
34
|
+
attr_accessor_with_fallback :openrouter_client
|
35
|
+
|
36
|
+
# The openai_client option determines the OpenAI client to use for communication.
|
37
|
+
attr_accessor_with_fallback :openai_client
|
38
|
+
|
39
|
+
DEFAULT_MAX_TOKENS = 1000
|
40
|
+
DEFAULT_MAX_COMPLETION_TOKENS = 16_384
|
41
|
+
DEFAULT_MODEL = "meta-llama/llama-3-8b-instruct:free"
|
42
|
+
DEFAULT_TEMPERATURE = 0.0
|
43
|
+
|
44
|
+
# Initializes a new instance of the Configuration class with default values.
|
45
|
+
def initialize(fallback: nil)
|
46
|
+
self.temperature = DEFAULT_TEMPERATURE
|
47
|
+
self.max_completion_tokens = DEFAULT_MAX_COMPLETION_TOKENS
|
48
|
+
self.max_tokens = DEFAULT_MAX_TOKENS
|
49
|
+
self.model = DEFAULT_MODEL
|
50
|
+
self.fallback = fallback
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
attr_accessor :fallback
|
56
|
+
|
57
|
+
def get_with_fallback(method)
|
58
|
+
value = instance_variable_get("@#{method}")
|
59
|
+
return value if value
|
60
|
+
return unless fallback
|
61
|
+
|
62
|
+
fallback.public_send(method)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/raix/mcp.rb
CHANGED
@@ -14,9 +14,10 @@
|
|
14
14
|
require "active_support/concern"
|
15
15
|
require "active_support/inflector"
|
16
16
|
require "securerandom"
|
17
|
-
require "faraday"
|
18
17
|
require "uri"
|
19
|
-
|
18
|
+
|
19
|
+
require_relative "../mcp/sse_client"
|
20
|
+
require_relative "../mcp/stdio_client"
|
20
21
|
|
21
22
|
module Raix
|
22
23
|
# Model Context Protocol integration for Raix
|
@@ -28,15 +29,38 @@ module Raix
|
|
28
29
|
module MCP
|
29
30
|
extend ActiveSupport::Concern
|
30
31
|
|
32
|
+
# Error raised when there's a protocol-level error in MCP communication
|
33
|
+
class ProtocolError < StandardError; end
|
34
|
+
|
31
35
|
JSONRPC_VERSION = "2.0".freeze
|
32
|
-
PROTOCOL_VERSION = "2024-11-05".freeze # Current supported protocol version
|
33
|
-
CONNECTION_TIMEOUT = 10
|
34
|
-
OPEN_TIMEOUT = 30
|
35
36
|
|
36
37
|
class_methods do
|
37
|
-
# Declare an MCP server by URL.
|
38
|
+
# Declare an MCP server by URL, using the SSE transport.
|
38
39
|
#
|
39
|
-
#
|
40
|
+
# sse_mcp "https://server.example.com/sse",
|
41
|
+
# headers: { "Authorization" => "Bearer <token>" },
|
42
|
+
# only: [:get_issue]
|
43
|
+
#
|
44
|
+
def sse_mcp(url, headers: {}, only: nil, except: nil)
|
45
|
+
mcp(only:, except:, client: MCP::SseClient.new(url, headers:))
|
46
|
+
end
|
47
|
+
|
48
|
+
# Declare an MCP server by command line arguments, and environment variables ,
|
49
|
+
# using the stdio transport.
|
50
|
+
#
|
51
|
+
# stdio_mcp "docker", "run", "-i", "--rm",
|
52
|
+
# "-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
|
53
|
+
# "ghcr.io/github/github-mcp-server",
|
54
|
+
# env: { GITHUB_PERSONAL_ACCESS_TOKEN: "${input:github_token}" },
|
55
|
+
# only: [:github_search]
|
56
|
+
#
|
57
|
+
def stdio_mcp(*args, env: {}, only: nil, except: nil)
|
58
|
+
mcp(only:, except:, client: MCP::StdioClient.new(*args, env))
|
59
|
+
end
|
60
|
+
|
61
|
+
# Declare an MCP server, using the given client.
|
62
|
+
#
|
63
|
+
# mcp client: MCP::SseClient.new("https://server.example.com/sse")
|
40
64
|
#
|
41
65
|
# This will automatically:
|
42
66
|
# • query `tools/list` on the server
|
@@ -46,72 +70,65 @@ module Raix
|
|
46
70
|
# call to the server and appends the proper messages to the
|
47
71
|
# transcript.
|
48
72
|
# NOTE TO SELF: NEVER MOCK SERVER RESPONSES! THIS MUST WORK WITH REAL SERVERS!
|
49
|
-
def mcp(
|
73
|
+
def mcp(client:, only: nil, except: nil)
|
50
74
|
@mcp_servers ||= {}
|
51
75
|
|
52
|
-
return if @mcp_servers.key?(
|
76
|
+
return if @mcp_servers.key?(client.unique_key) # avoid duplicate definitions
|
53
77
|
|
54
|
-
#
|
55
|
-
|
56
|
-
result = Thread::Queue.new
|
57
|
-
Thread.new do
|
58
|
-
establish_sse_connection(url, result:)
|
59
|
-
end
|
60
|
-
tools = result.pop
|
78
|
+
# Fetch tools
|
79
|
+
tools = client.tools
|
61
80
|
|
62
81
|
if tools.empty?
|
63
|
-
puts "[MCP DEBUG] No tools found from MCP server at #{url}"
|
82
|
+
# puts "[MCP DEBUG] No tools found from MCP server at #{url}"
|
83
|
+
client.close
|
64
84
|
return nil
|
65
85
|
end
|
66
86
|
|
67
|
-
# 3. Register each tool so ChatCompletion#tools picks them up
|
68
87
|
# Apply filters
|
69
88
|
filtered_tools = if only.present?
|
70
89
|
only_symbols = Array(only).map(&:to_sym)
|
71
|
-
tools.select { |tool| only_symbols.include?(tool
|
90
|
+
tools.select { |tool| only_symbols.include?(tool.name.to_sym) }
|
72
91
|
elsif except.present?
|
73
92
|
except_symbols = Array(except).map(&:to_sym)
|
74
|
-
tools.reject { |tool| except_symbols.include?(tool
|
93
|
+
tools.reject { |tool| except_symbols.include?(tool.name.to_sym) }
|
75
94
|
else
|
76
95
|
tools
|
77
96
|
end
|
78
97
|
|
79
98
|
# Ensure FunctionDispatch is included in the class
|
80
|
-
# Explicit include in the class context
|
81
99
|
include FunctionDispatch unless included_modules.include?(FunctionDispatch)
|
82
|
-
puts "[MCP DEBUG] FunctionDispatch included in #{name}"
|
100
|
+
# puts "[MCP DEBUG] FunctionDispatch included in #{name}"
|
83
101
|
|
84
102
|
filtered_tools.each do |tool|
|
85
|
-
remote_name = tool
|
103
|
+
remote_name = tool.name
|
86
104
|
# TODO: Revisit later whether this much context is needed in the function name
|
87
|
-
local_name = "#{
|
105
|
+
local_name = "#{client.unique_key}_#{remote_name}".to_sym
|
88
106
|
|
89
|
-
description
|
90
|
-
input_schema = tool
|
107
|
+
description = tool.description
|
108
|
+
input_schema = tool.input_schema || {}
|
91
109
|
|
92
110
|
# --- register with FunctionDispatch (adds to .functions)
|
93
111
|
function(local_name, description, **{}) # placeholder parameters replaced next
|
94
112
|
latest_definition = functions.last
|
95
|
-
latest_definition[:parameters] = input_schema.deep_symbolize_keys
|
113
|
+
latest_definition[:parameters] = input_schema.deep_symbolize_keys || {}
|
114
|
+
|
115
|
+
# Required by OpenAI
|
116
|
+
latest_definition[:parameters][:properties] ||= {}
|
117
|
+
|
118
|
+
# Store the schema for type coercion
|
119
|
+
tool_schemas = @tool_schemas ||= {}
|
120
|
+
tool_schemas[local_name] = input_schema
|
96
121
|
|
97
122
|
# --- define an instance method that proxies to the server
|
98
|
-
define_method(local_name) do
|
123
|
+
define_method(local_name) do |arguments, _cache|
|
99
124
|
arguments ||= {}
|
100
125
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
self.class.establish_sse_connection(url, name: remote_name, arguments:, result:)
|
105
|
-
end
|
106
|
-
|
107
|
-
content_item = result.pop
|
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)
|
108
129
|
|
109
|
-
|
110
|
-
|
111
|
-
content_item["text"]
|
112
|
-
else
|
113
|
-
content_item.to_json
|
114
|
-
end
|
130
|
+
content_text = client.call_tool(remote_name, **coerced_arguments)
|
131
|
+
call_id = SecureRandom.uuid
|
115
132
|
|
116
133
|
# Mirror FunctionDispatch transcript behaviour
|
117
134
|
transcript << [
|
@@ -144,193 +161,95 @@ module Raix
|
|
144
161
|
end
|
145
162
|
end
|
146
163
|
|
147
|
-
# Store the URL and
|
148
|
-
@mcp_servers[
|
164
|
+
# Store the URL, tools, and client for future use
|
165
|
+
@mcp_servers[client.unique_key] = { tools: filtered_tools, client: }
|
149
166
|
end
|
167
|
+
end
|
150
168
|
|
151
|
-
|
152
|
-
# advertised by the server. The MCP specification allows two different event
|
153
|
-
# formats during initialization:
|
154
|
-
#
|
155
|
-
# 1. A generic JSON‑RPC *initialize* event (the behaviour previously
|
156
|
-
# implemented):
|
157
|
-
#
|
158
|
-
# event: message (implicit when no explicit event type is given)
|
159
|
-
# data: {"jsonrpc":"2.0","method":"initialize","params":{"endpoint_url":"https://…/rpc"}}
|
160
|
-
#
|
161
|
-
# 2. A dedicated *endpoint* event, as implemented by the reference
|
162
|
-
# TypeScript SDK and the public GitMCP server used in our test-suite:
|
163
|
-
#
|
164
|
-
# event: endpoint\n
|
165
|
-
# data: /rpc\n
|
166
|
-
#
|
167
|
-
# This method now supports **both** formats.
|
168
|
-
#
|
169
|
-
# It uses Net::HTTP directly rather than Faraday streaming because the latter
|
170
|
-
# does not consistently surface partial body reads across adapters. The
|
171
|
-
# implementation reads the response body incrementally, splitting on the
|
172
|
-
# SSE record delimiter (double newline) and processing each event until an
|
173
|
-
# endpoint is discovered (or a timeout / connection error occurs).
|
174
|
-
def establish_sse_connection(url, name: nil, arguments: {}, result: nil)
|
175
|
-
puts "[MCP DEBUG] Establishing MCP connection with URL: #{url}"
|
176
|
-
|
177
|
-
headers = {
|
178
|
-
"Accept" => "text/event-stream",
|
179
|
-
"Cache-Control" => "no-cache",
|
180
|
-
"Connection" => "keep-alive",
|
181
|
-
"MCP-Version" => PROTOCOL_VERSION
|
182
|
-
}
|
183
|
-
|
184
|
-
endpoint_url = nil
|
185
|
-
buffer = ""
|
186
|
-
|
187
|
-
connection = Faraday.new(url:) do |faraday|
|
188
|
-
faraday.options.timeout = CONNECTION_TIMEOUT
|
189
|
-
faraday.options.open_timeout = OPEN_TIMEOUT
|
190
|
-
end
|
169
|
+
private
|
191
170
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
buffer << chunk
|
196
|
-
|
197
|
-
# Process complete SSE events (separated by a blank line)
|
198
|
-
while (idx = buffer.index("\n\n"))
|
199
|
-
event_text = buffer.slice!(0..idx + 1) # include delimiter
|
200
|
-
event_type, event_data = parse_sse_fields(event_text)
|
201
|
-
|
202
|
-
case event_type
|
203
|
-
when "endpoint"
|
204
|
-
# event data is expected to be a plain string with the endpoint
|
205
|
-
puts "[MCP DEBUG] Found endpoint event: #{event_data}"
|
206
|
-
endpoint_url = build_absolute_url(url, event_data)
|
207
|
-
initialize_mcp_connection(connection, endpoint_url)
|
208
|
-
when "message"
|
209
|
-
puts "[MCP DEBUG] Received message: #{event_data}"
|
210
|
-
dispatch_event(event_data, connection, endpoint_url, name, arguments, result)
|
211
|
-
else
|
212
|
-
puts "[MCP DEBUG] Unexpected event type: #{event_type} with data: #{event_data}"
|
213
|
-
end
|
214
|
-
end
|
215
|
-
end
|
216
|
-
end
|
217
|
-
end
|
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)
|
218
174
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
event_text.each_line do |line|
|
228
|
-
case line
|
229
|
-
when /^event:\s*(.+)$/
|
230
|
-
event_type = Regexp.last_match(1).strip
|
231
|
-
when /^data:\s*(.*)$/
|
232
|
-
data_lines << Regexp.last_match(1)
|
233
|
-
end
|
234
|
-
end
|
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?
|
235
183
|
|
236
|
-
[
|
184
|
+
coerced[key] = coerce_value(value, prop_schema)
|
237
185
|
end
|
238
186
|
|
239
|
-
#
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
return candidate if uri.absolute?
|
244
|
-
|
245
|
-
URI.join(base, candidate).to_s
|
246
|
-
rescue URI::InvalidURIError
|
247
|
-
candidate # fall back to original string
|
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)
|
248
191
|
end
|
249
192
|
|
250
|
-
|
251
|
-
|
252
|
-
connection.post(endpoint_url) do |req|
|
253
|
-
req.headers["Content-Type"] = "application/json"
|
254
|
-
req.body = {
|
255
|
-
jsonrpc: JSONRPC_VERSION,
|
256
|
-
id: SecureRandom.uuid,
|
257
|
-
method: "initialize",
|
258
|
-
params: {
|
259
|
-
protocolVersion: PROTOCOL_VERSION,
|
260
|
-
capabilities: {
|
261
|
-
roots: {
|
262
|
-
listChanged: true
|
263
|
-
},
|
264
|
-
sampling: {}
|
265
|
-
},
|
266
|
-
clientInfo: {
|
267
|
-
name: "Raix",
|
268
|
-
version: Raix::VERSION
|
269
|
-
}
|
270
|
-
}
|
271
|
-
}.to_json
|
272
|
-
end
|
273
|
-
end
|
193
|
+
coerced.with_indifferent_access
|
194
|
+
end
|
274
195
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
puts "[MCP DEBUG] Received tools event: #{tools}"
|
284
|
-
if name.present?
|
285
|
-
puts "[MCP DEBUG] Calling function: #{name} with params: #{arguments.inspect}"
|
286
|
-
remote_dispatch(connection, endpoint_url, name, arguments)
|
287
|
-
else
|
288
|
-
result << tools # will unblock the pop on the main thread
|
289
|
-
connection.close
|
290
|
-
end
|
291
|
-
in { result: { content: } }
|
292
|
-
puts "[MCP DEBUG] Received content event: #{content}"
|
293
|
-
result << content # will unblock the pop on the main thread
|
294
|
-
connection.close
|
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
|
295
204
|
else
|
296
|
-
|
205
|
+
value
|
297
206
|
end
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
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
|
309
218
|
end
|
310
|
-
end
|
311
219
|
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
jsonrpc: JSONRPC_VERSION,
|
318
|
-
method: "notifications/initialized"
|
319
|
-
}.to_json
|
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
|
320
225
|
end
|
321
|
-
|
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
|
322
240
|
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
}.to_json
|
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
|
333
250
|
end
|
251
|
+
else
|
252
|
+
value
|
334
253
|
end
|
335
254
|
end
|
336
255
|
end
|
data/lib/raix/predicate.rb
CHANGED
@@ -26,12 +26,9 @@ module Raix
|
|
26
26
|
# question = Question.new
|
27
27
|
# question.ask("Is Ruby a programming language?")
|
28
28
|
module Predicate
|
29
|
+
extend ActiveSupport::Concern
|
29
30
|
include ChatCompletion
|
30
31
|
|
31
|
-
def self.included(base)
|
32
|
-
base.extend(ClassMethods)
|
33
|
-
end
|
34
|
-
|
35
32
|
def ask(question, openai: false)
|
36
33
|
raise "Please define a yes and/or no block" if self.class.yes_block.nil? && self.class.no_block.nil?
|
37
34
|
|
data/lib/raix/version.rb
CHANGED
data/lib/raix.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "raix/version"
|
4
|
+
require_relative "raix/configuration"
|
4
5
|
require_relative "raix/chat_completion"
|
5
6
|
require_relative "raix/function_dispatch"
|
6
7
|
require_relative "raix/prompt_declarations"
|
@@ -10,42 +11,6 @@ require_relative "raix/mcp"
|
|
10
11
|
|
11
12
|
# The Raix module provides configuration options for the Raix gem.
|
12
13
|
module Raix
|
13
|
-
# The Configuration class holds the configuration options for the Raix gem.
|
14
|
-
class Configuration
|
15
|
-
# The temperature option determines the randomness of the generated text.
|
16
|
-
# Higher values result in more random output.
|
17
|
-
attr_accessor :temperature
|
18
|
-
|
19
|
-
# The max_tokens option determines the maximum number of tokens to generate.
|
20
|
-
attr_accessor :max_tokens
|
21
|
-
|
22
|
-
# The max_completion_tokens option determines the maximum number of tokens to generate.
|
23
|
-
attr_accessor :max_completion_tokens
|
24
|
-
|
25
|
-
# The model option determines the model to use for text generation. This option
|
26
|
-
# is normally set in each class that includes the ChatCompletion module.
|
27
|
-
attr_accessor :model
|
28
|
-
|
29
|
-
# The openrouter_client option determines the default client to use for communicatio.
|
30
|
-
attr_accessor :openrouter_client
|
31
|
-
|
32
|
-
# The openai_client option determines the OpenAI client to use for communication.
|
33
|
-
attr_accessor :openai_client
|
34
|
-
|
35
|
-
DEFAULT_MAX_TOKENS = 1000
|
36
|
-
DEFAULT_MAX_COMPLETION_TOKENS = 16_384
|
37
|
-
DEFAULT_MODEL = "meta-llama/llama-3-8b-instruct:free"
|
38
|
-
DEFAULT_TEMPERATURE = 0.0
|
39
|
-
|
40
|
-
# Initializes a new instance of the Configuration class with default values.
|
41
|
-
def initialize
|
42
|
-
self.temperature = DEFAULT_TEMPERATURE
|
43
|
-
self.max_completion_tokens = DEFAULT_MAX_COMPLETION_TOKENS
|
44
|
-
self.max_tokens = DEFAULT_MAX_TOKENS
|
45
|
-
self.model = DEFAULT_MODEL
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
14
|
class << self
|
50
15
|
attr_writer :configuration
|
51
16
|
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.
|
4
|
+
version: 0.9.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Obie Fernandez
|
@@ -83,8 +83,12 @@ files:
|
|
83
83
|
- README.llm
|
84
84
|
- README.md
|
85
85
|
- Rakefile
|
86
|
+
- lib/mcp/sse_client.rb
|
87
|
+
- lib/mcp/stdio_client.rb
|
88
|
+
- lib/mcp/tool.rb
|
86
89
|
- lib/raix.rb
|
87
90
|
- lib/raix/chat_completion.rb
|
91
|
+
- lib/raix/configuration.rb
|
88
92
|
- lib/raix/function_dispatch.rb
|
89
93
|
- lib/raix/mcp.rb
|
90
94
|
- lib/raix/message_adapters/base.rb
|