raix 1.0.3 → 2.0.0
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 +41 -0
- data/Gemfile.lock +15 -12
- data/README.md +184 -28
- data/lib/mcp/sse_client.rb +1 -1
- data/lib/raix/chat_completion.rb +138 -49
- data/lib/raix/completion_context.rb +36 -0
- data/lib/raix/configuration.rb +18 -3
- data/lib/raix/function_tool_adapter.rb +51 -0
- data/lib/raix/transcript_adapter.rb +121 -0
- data/lib/raix/version.rb +1 -1
- data/lib/raix.rb +6 -1
- metadata +8 -20
- data/raix.gemspec +0 -36
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a72cfc7cfe3564db566d644088ca64b8076898a87517da3d2a124123027717a8
|
|
4
|
+
data.tar.gz: e956d819cc90487a26abc5d1005ef4311d010f95fe101cc45047af331129b747
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 144cc757f49905ba32b5928a31a60449d8013390d365b9e16b6d88ef0e8c4eb30188a5c96fe3099890e957abae087aef00bc35a578f3ffa5d4b009103219f3cf
|
|
7
|
+
data.tar.gz: eca2ca58a34345fc785066482f0b00871cb105e02ff3edae3e3471b5e813fd9e15737bf5e6676374d78d961be64f170c0adc9e62cc285713aff4c90dd5f148ab
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
## [2.0.0] - 2025-12-17
|
|
2
|
+
|
|
3
|
+
### Breaking Changes
|
|
4
|
+
- **Migrated from OpenRouter/OpenAI gems to RubyLLM** - Raix now uses [RubyLLM](https://github.com/crmne/ruby_llm) as its unified backend for all LLM providers. This provides better multi-provider support and a more consistent API.
|
|
5
|
+
- **Configuration changes** - API keys are now configured through RubyLLM's configuration system instead of separate client instances.
|
|
6
|
+
- **Removed direct client dependencies** - `openrouter` and `ruby-openai` gems are no longer direct dependencies; RubyLLM handles provider connections.
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- **`before_completion` hook** - New hook system for intercepting and modifying chat completion requests before they're sent to the AI provider.
|
|
10
|
+
- Configure at global, class, or instance levels
|
|
11
|
+
- Hooks receive a `CompletionContext` with access to messages, params, and the chat completion instance
|
|
12
|
+
- Messages are mutable for content filtering, PII redaction, adding system prompts, etc.
|
|
13
|
+
- Params can be modified for dynamic model selection, A/B testing, and more
|
|
14
|
+
- Supports any callable object (Proc, Lambda, or object responding to `#call`)
|
|
15
|
+
- Use cases: database-backed configuration, logging, PII redaction, content filtering, cost tracking
|
|
16
|
+
- **`FunctionToolAdapter`** - New adapter for converting Raix function declarations to RubyLLM tool format
|
|
17
|
+
- **`TranscriptAdapter`** - New adapter for bridging Raix's abbreviated message format with standard OpenAI format
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Chat completions now use RubyLLM's unified API for all providers (OpenAI, Anthropic, Google, etc.)
|
|
21
|
+
- Improved provider detection based on model name patterns
|
|
22
|
+
- Streamlined internal architecture with dedicated adapters
|
|
23
|
+
|
|
24
|
+
### Migration Guide
|
|
25
|
+
Update your configuration from:
|
|
26
|
+
```ruby
|
|
27
|
+
Raix.configure do |config|
|
|
28
|
+
config.openrouter_client = OpenRouter::Client.new(access_token: "...")
|
|
29
|
+
config.openai_client = OpenAI::Client.new(access_token: "...")
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
To:
|
|
34
|
+
```ruby
|
|
35
|
+
RubyLLM.configure do |config|
|
|
36
|
+
config.openrouter_api_key = ENV["OPENROUTER_API_KEY"]
|
|
37
|
+
config.openai_api_key = ENV["OPENAI_API_KEY"]
|
|
38
|
+
# Also supports: anthropic_api_key, gemini_api_key
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
1
42
|
## [1.0.2] - 2025-07-16
|
|
2
43
|
### Added
|
|
3
44
|
- Added method to check for API client availability in Configuration
|
data/Gemfile.lock
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
raix (
|
|
4
|
+
raix (2.0.0)
|
|
5
5
|
activesupport (>= 6.0)
|
|
6
6
|
faraday-retry (~> 2.0)
|
|
7
|
-
open_router (~> 0.2)
|
|
8
7
|
ostruct
|
|
9
|
-
|
|
8
|
+
ruby_llm (~> 1.9)
|
|
10
9
|
|
|
11
10
|
GEM
|
|
12
11
|
remote: https://rubygems.org/
|
|
@@ -78,6 +77,7 @@ GEM
|
|
|
78
77
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
|
79
78
|
rb-inotify (~> 0.9, >= 0.9.10)
|
|
80
79
|
lumberjack (1.2.10)
|
|
80
|
+
marcel (1.1.0)
|
|
81
81
|
method_source (1.1.0)
|
|
82
82
|
minitest (5.24.0)
|
|
83
83
|
multipart-post (2.4.1)
|
|
@@ -93,11 +93,6 @@ GEM
|
|
|
93
93
|
notiffany (0.1.3)
|
|
94
94
|
nenv (~> 0.1)
|
|
95
95
|
shellany (~> 0.0)
|
|
96
|
-
open_router (0.3.3)
|
|
97
|
-
activesupport (>= 6.0)
|
|
98
|
-
dotenv (>= 2)
|
|
99
|
-
faraday (>= 1)
|
|
100
|
-
faraday-multipart (>= 1)
|
|
101
96
|
ostruct (0.6.1)
|
|
102
97
|
parallel (1.24.0)
|
|
103
98
|
parser (3.3.0.5)
|
|
@@ -148,11 +143,18 @@ GEM
|
|
|
148
143
|
unicode-display_width (>= 2.4.0, < 3.0)
|
|
149
144
|
rubocop-ast (1.31.2)
|
|
150
145
|
parser (>= 3.3.0.4)
|
|
151
|
-
ruby-openai (8.1.0)
|
|
152
|
-
event_stream_parser (>= 0.3.0, < 2.0.0)
|
|
153
|
-
faraday (>= 1)
|
|
154
|
-
faraday-multipart (>= 1)
|
|
155
146
|
ruby-progressbar (1.13.0)
|
|
147
|
+
ruby_llm (1.9.1)
|
|
148
|
+
base64
|
|
149
|
+
event_stream_parser (~> 1)
|
|
150
|
+
faraday (>= 1.10.0)
|
|
151
|
+
faraday-multipart (>= 1)
|
|
152
|
+
faraday-net_http (>= 1)
|
|
153
|
+
faraday-retry (>= 1)
|
|
154
|
+
marcel (~> 1.0)
|
|
155
|
+
ruby_llm-schema (~> 0.2.1)
|
|
156
|
+
zeitwerk (~> 2)
|
|
157
|
+
ruby_llm-schema (0.2.5)
|
|
156
158
|
shellany (0.0.1)
|
|
157
159
|
solargraph (0.50.0)
|
|
158
160
|
backport (~> 1.2)
|
|
@@ -210,6 +212,7 @@ GEM
|
|
|
210
212
|
yard-sorbet (0.8.1)
|
|
211
213
|
sorbet-runtime (>= 0.5)
|
|
212
214
|
yard (>= 0.9)
|
|
215
|
+
zeitwerk (2.7.3)
|
|
213
216
|
|
|
214
217
|
PLATFORMS
|
|
215
218
|
arm64-darwin-21
|
data/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Raix (pronounced "ray" because the x is silent) is a library that gives you ever
|
|
|
6
6
|
|
|
7
7
|
Understanding how to use discrete AI components in otherwise normal code is key to productively leveraging Raix, and the subject of a book written by Raix's author Obie Fernandez, titled [Patterns of Application Development Using AI](https://leanpub.com/patterns-of-application-development-using-ai). You can easily support the ongoing development of this project by buying the book at Leanpub.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Raix 2.0 is powered by [RubyLLM](https://github.com/crmne/ruby_llm), giving you unified access to OpenAI, Anthropic, Google Gemini, and dozens of other providers through OpenRouter. Note that you can use Raix to add AI capabilities to non-Rails applications as long as you include ActiveSupport as a dependency.
|
|
10
10
|
|
|
11
11
|
### Chat Completions
|
|
12
12
|
|
|
@@ -105,6 +105,148 @@ When using JSON mode with non-OpenAI providers, Raix automatically sets the `req
|
|
|
105
105
|
=> { "key": "value" }
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
### before_completion Hook
|
|
109
|
+
|
|
110
|
+
The `before_completion` hook lets you intercept and modify chat completion requests before they're sent to the AI provider. This is useful for dynamic parameter resolution, logging, content filtering, PII redaction, and more.
|
|
111
|
+
|
|
112
|
+
#### Configuration Levels
|
|
113
|
+
|
|
114
|
+
Hooks can be configured at three levels, with later levels overriding earlier ones:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
# Global level - applies to all chat completions
|
|
118
|
+
Raix.configure do |config|
|
|
119
|
+
config.before_completion = ->(context) {
|
|
120
|
+
# Return a hash of params to merge, or modify context.messages directly
|
|
121
|
+
{ temperature: 0.7 }
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Class level - applies to all instances of a class
|
|
126
|
+
class MyAssistant
|
|
127
|
+
include Raix::ChatCompletion
|
|
128
|
+
|
|
129
|
+
configure do |config|
|
|
130
|
+
config.before_completion = ->(context) { { model: "gpt-4o" } }
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Instance level - applies to a single instance
|
|
135
|
+
assistant = MyAssistant.new
|
|
136
|
+
assistant.before_completion = ->(context) { { max_tokens: 500 } }
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
When hooks exist at multiple levels, they're called in order (global → class → instance), with returned params merged together. Later hooks override earlier ones for the same parameter.
|
|
140
|
+
|
|
141
|
+
#### The CompletionContext Object
|
|
142
|
+
|
|
143
|
+
Hooks receive a `CompletionContext` object with access to:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
context.chat_completion # The ChatCompletion instance
|
|
147
|
+
context.messages # Array of messages (mutable, in OpenAI format)
|
|
148
|
+
context.params # Hash of params (mutable)
|
|
149
|
+
context.transcript # The instance's transcript
|
|
150
|
+
context.current_model # Currently configured model
|
|
151
|
+
context.chat_completion_class # The class including ChatCompletion
|
|
152
|
+
context.configuration # The instance's configuration
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### Use Cases
|
|
156
|
+
|
|
157
|
+
**Dynamic model selection from database:**
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
Raix.configure do |config|
|
|
161
|
+
config.before_completion = ->(context) {
|
|
162
|
+
settings = TenantSettings.find_by(tenant: Current.tenant)
|
|
163
|
+
{
|
|
164
|
+
model: settings.preferred_model,
|
|
165
|
+
temperature: settings.temperature,
|
|
166
|
+
max_tokens: settings.max_tokens
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**PII redaction:**
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
class SecureAssistant
|
|
176
|
+
include Raix::ChatCompletion
|
|
177
|
+
|
|
178
|
+
before_completion = ->(context) {
|
|
179
|
+
context.messages.each do |msg|
|
|
180
|
+
next unless msg[:content].is_a?(String)
|
|
181
|
+
# Redact SSN patterns
|
|
182
|
+
msg[:content] = msg[:content].gsub(/\d{3}-\d{2}-\d{4}/, "[SSN REDACTED]")
|
|
183
|
+
# Redact email addresses
|
|
184
|
+
msg[:content] = msg[:content].gsub(/[\w.-]+@[\w.-]+\.\w+/, "[EMAIL REDACTED]")
|
|
185
|
+
end
|
|
186
|
+
{} # Return empty hash if not modifying params
|
|
187
|
+
}
|
|
188
|
+
end
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Request logging:**
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
Raix.configure do |config|
|
|
195
|
+
config.before_completion = ->(context) {
|
|
196
|
+
Rails.logger.info({
|
|
197
|
+
event: "chat_completion_request",
|
|
198
|
+
model: context.current_model,
|
|
199
|
+
message_count: context.messages.length,
|
|
200
|
+
params: context.params.except(:messages)
|
|
201
|
+
}.to_json)
|
|
202
|
+
{} # Return empty hash, just logging
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Adding system prompts:**
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
assistant.before_completion = ->(context) {
|
|
211
|
+
context.messages.unshift({
|
|
212
|
+
role: "system",
|
|
213
|
+
content: "Always be helpful and respectful."
|
|
214
|
+
})
|
|
215
|
+
{}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**A/B testing models:**
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
Raix.configure do |config|
|
|
223
|
+
config.before_completion = ->(context) {
|
|
224
|
+
if Flipper.enabled?(:new_model, Current.user)
|
|
225
|
+
{ model: "gpt-4o" }
|
|
226
|
+
else
|
|
227
|
+
{ model: "gpt-4o-mini" }
|
|
228
|
+
end
|
|
229
|
+
}
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Hooks can also be any object that responds to `#call`:
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
class CostTracker
|
|
237
|
+
def call(context)
|
|
238
|
+
# Track estimated cost based on message length
|
|
239
|
+
estimated_tokens = context.messages.sum { |m| m[:content].to_s.length / 4 }
|
|
240
|
+
StatsD.gauge("ai.estimated_input_tokens", estimated_tokens)
|
|
241
|
+
{}
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
Raix.configure do |config|
|
|
246
|
+
config.before_completion = CostTracker.new
|
|
247
|
+
end
|
|
248
|
+
```
|
|
249
|
+
|
|
108
250
|
### Use of Tools/Functions
|
|
109
251
|
|
|
110
252
|
The second (optional) module that you can add to your Ruby classes after `ChatCompletion` is `FunctionDispatch`. It lets you declare and implement functions to be called at the AI's discretion in a declarative, Rails-like "DSL" fashion.
|
|
@@ -711,49 +853,63 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
|
711
853
|
|
|
712
854
|
$ gem install raix
|
|
713
855
|
|
|
714
|
-
|
|
856
|
+
### Configuration
|
|
715
857
|
|
|
716
|
-
|
|
858
|
+
Raix 2.0 uses [RubyLLM](https://github.com/crmne/ruby_llm) as its backend for LLM provider connections. Configure your API keys through RubyLLM:
|
|
717
859
|
|
|
718
860
|
```ruby
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
end
|
|
728
|
-
|
|
729
|
-
Raix.configure do |config|
|
|
730
|
-
config.openrouter_client = OpenRouter::Client.new(access_token: ENV.fetch("OR_ACCESS_TOKEN", nil))
|
|
731
|
-
config.openai_client = OpenAI::Client.new(access_token: ENV.fetch("OAI_ACCESS_TOKEN", nil)) do |f|
|
|
732
|
-
f.request :retry, retry_options
|
|
733
|
-
f.response :logger, Logger.new($stdout), { headers: true, bodies: true, errors: true } do |logger|
|
|
734
|
-
logger.filter(/(Bearer) (\S+)/, '\1[REDACTED]')
|
|
735
|
-
end
|
|
736
|
-
end
|
|
737
|
-
end
|
|
861
|
+
# config/initializers/raix.rb
|
|
862
|
+
RubyLLM.configure do |config|
|
|
863
|
+
config.openrouter_api_key = ENV["OPENROUTER_API_KEY"]
|
|
864
|
+
config.openai_api_key = ENV["OPENAI_API_KEY"]
|
|
865
|
+
# Optional: configure other providers
|
|
866
|
+
# config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
|
|
867
|
+
# config.gemini_api_key = ENV["GEMINI_API_KEY"]
|
|
868
|
+
end
|
|
738
869
|
```
|
|
739
870
|
|
|
740
|
-
|
|
871
|
+
Raix will automatically use the appropriate provider based on the model name:
|
|
872
|
+
- Models starting with `gpt-` or `o1` use OpenAI directly
|
|
873
|
+
- All other models route through OpenRouter
|
|
741
874
|
|
|
742
|
-
### Global vs
|
|
875
|
+
### Global vs Class-Level Configuration
|
|
743
876
|
|
|
744
|
-
You can
|
|
745
|
-
same syntax:
|
|
877
|
+
You can configure Raix options globally or at the class level:
|
|
746
878
|
|
|
747
879
|
```ruby
|
|
748
|
-
|
|
880
|
+
# Global configuration
|
|
881
|
+
Raix.configure do |config|
|
|
882
|
+
config.temperature = 0.7
|
|
883
|
+
config.max_tokens = 1000
|
|
884
|
+
config.model = "gpt-4o"
|
|
885
|
+
config.max_tool_calls = 25
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
# Class-level configuration (overrides global)
|
|
889
|
+
class MyAssistant
|
|
749
890
|
include Raix::ChatCompletion
|
|
750
891
|
|
|
751
892
|
configure do |config|
|
|
752
|
-
config.
|
|
893
|
+
config.model = "anthropic/claude-3-opus"
|
|
894
|
+
config.temperature = 0.5
|
|
753
895
|
end
|
|
754
896
|
end
|
|
755
897
|
```
|
|
756
898
|
|
|
899
|
+
### Upgrading from Raix 1.x
|
|
900
|
+
|
|
901
|
+
If upgrading from Raix 1.x, update your configuration from:
|
|
902
|
+
|
|
903
|
+
```ruby
|
|
904
|
+
# Old 1.x configuration
|
|
905
|
+
Raix.configure do |config|
|
|
906
|
+
config.openrouter_client = OpenRouter::Client.new(access_token: "...")
|
|
907
|
+
config.openai_client = OpenAI::Client.new(access_token: "...")
|
|
908
|
+
end
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
To the new RubyLLM-based configuration shown above.
|
|
912
|
+
|
|
757
913
|
## Development
|
|
758
914
|
|
|
759
915
|
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.
|
data/lib/mcp/sse_client.rb
CHANGED
|
@@ -138,7 +138,7 @@ module Raix
|
|
|
138
138
|
# Process SSE buffer for complete events
|
|
139
139
|
def process_sse_buffer
|
|
140
140
|
while (idx = @buffer.index("\n\n"))
|
|
141
|
-
event_text = @buffer.slice!(0..idx + 1)
|
|
141
|
+
event_text = @buffer.slice!(0..(idx + 1))
|
|
142
142
|
event_type, event_data = parse_sse_fields(event_text)
|
|
143
143
|
|
|
144
144
|
case event_type
|
data/lib/raix/chat_completion.rb
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
require "active_support/concern"
|
|
4
4
|
require "active_support/core_ext/object/blank"
|
|
5
5
|
require "active_support/core_ext/string/filters"
|
|
6
|
-
require "
|
|
7
|
-
require "
|
|
6
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
7
|
+
require "ruby_llm"
|
|
8
8
|
|
|
9
9
|
require_relative "message_adapters/base"
|
|
10
|
+
require_relative "transcript_adapter"
|
|
11
|
+
require_relative "function_tool_adapter"
|
|
10
12
|
|
|
11
13
|
module Raix
|
|
12
14
|
class UndeclaredToolError < StandardError; end
|
|
@@ -40,10 +42,10 @@ module Raix
|
|
|
40
42
|
module ChatCompletion
|
|
41
43
|
extend ActiveSupport::Concern
|
|
42
44
|
|
|
43
|
-
attr_accessor :cache_at, :frequency_penalty, :logit_bias, :logprobs, :loop, :min_p, :model,
|
|
44
|
-
:prediction, :repetition_penalty, :response_format, :stream, :temperature,
|
|
45
|
-
:max_tokens, :seed, :stop, :top_a, :top_k, :top_logprobs, :top_p, :tools,
|
|
46
|
-
:max_tool_calls, :stop_tool_calls_and_respond
|
|
45
|
+
attr_accessor :before_completion, :cache_at, :frequency_penalty, :logit_bias, :logprobs, :loop, :min_p, :model,
|
|
46
|
+
:presence_penalty, :prediction, :repetition_penalty, :response_format, :stream, :temperature,
|
|
47
|
+
:max_completion_tokens, :max_tokens, :seed, :stop, :top_a, :top_k, :top_logprobs, :top_p, :tools,
|
|
48
|
+
:available_tools, :tool_choice, :provider, :max_tool_calls, :stop_tool_calls_and_respond
|
|
47
49
|
|
|
48
50
|
class_methods do
|
|
49
51
|
# Returns the current configuration of this class. Falls back to global configuration for unset values.
|
|
@@ -142,12 +144,12 @@ module Raix
|
|
|
142
144
|
messages = messages.map { |msg| adapter.transform(msg) }.dup
|
|
143
145
|
raise "Can't complete an empty transcript" if messages.blank?
|
|
144
146
|
|
|
147
|
+
# Run before_completion hooks (global -> class -> instance)
|
|
148
|
+
# Hooks can modify params and messages for logging, filtering, PII redaction, etc.
|
|
149
|
+
run_before_completion_hooks(params, messages)
|
|
150
|
+
|
|
145
151
|
begin
|
|
146
|
-
response =
|
|
147
|
-
openai_request(params:, model: openai, messages:)
|
|
148
|
-
else
|
|
149
|
-
openrouter_request(params:, model:, messages:)
|
|
150
|
-
end
|
|
152
|
+
response = ruby_llm_request(params:, model: openai || model, messages:, openai_override: openai)
|
|
151
153
|
retry_count = 0
|
|
152
154
|
content = nil
|
|
153
155
|
|
|
@@ -155,7 +157,7 @@ module Raix
|
|
|
155
157
|
return if stream && response.blank?
|
|
156
158
|
|
|
157
159
|
# tuck the full response into a thread local in case needed
|
|
158
|
-
Thread.current[:chat_completion_response] = response.with_indifferent_access
|
|
160
|
+
Thread.current[:chat_completion_response] = response.is_a?(Hash) ? response.with_indifferent_access : response
|
|
159
161
|
|
|
160
162
|
# TODO: add a standardized callback hook for usage events
|
|
161
163
|
# broadcast(:usage_event, usage_subject, self.class.name.to_s, response, premium?)
|
|
@@ -171,11 +173,7 @@ module Raix
|
|
|
171
173
|
|
|
172
174
|
# Force a final response without tools
|
|
173
175
|
params[:tools] = nil
|
|
174
|
-
response =
|
|
175
|
-
openai_request(params:, model: openai, messages:)
|
|
176
|
-
else
|
|
177
|
-
openrouter_request(params:, model:, messages:)
|
|
178
|
-
end
|
|
176
|
+
response = ruby_llm_request(params:, model: openai || model, messages:, openai_override: openai)
|
|
179
177
|
|
|
180
178
|
# Process the final response
|
|
181
179
|
content = response.dig("choices", 0, "message", "content")
|
|
@@ -217,11 +215,7 @@ module Raix
|
|
|
217
215
|
elsif @stop_tool_calls_and_respond
|
|
218
216
|
# If stop_tool_calls_and_respond was set, force a final response without tools
|
|
219
217
|
params[:tools] = nil
|
|
220
|
-
response =
|
|
221
|
-
openai_request(params:, model: openai, messages:)
|
|
222
|
-
else
|
|
223
|
-
openrouter_request(params:, model:, messages:)
|
|
224
|
-
end
|
|
218
|
+
response = ruby_llm_request(params:, model: openai || model, messages:, openai_override: openai)
|
|
225
219
|
|
|
226
220
|
content = response.dig("choices", 0, "message", "content")
|
|
227
221
|
transcript << { assistant: content } if save_response
|
|
@@ -279,7 +273,23 @@ module Raix
|
|
|
279
273
|
#
|
|
280
274
|
# @return [Array] The transcript array.
|
|
281
275
|
def transcript
|
|
282
|
-
@transcript ||=
|
|
276
|
+
@transcript ||= TranscriptAdapter.new(ruby_llm_chat)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Returns the RubyLLM::Chat instance for this conversation
|
|
280
|
+
def ruby_llm_chat
|
|
281
|
+
@ruby_llm_chat ||= begin
|
|
282
|
+
model_id = model || configuration.model
|
|
283
|
+
|
|
284
|
+
# Determine provider based on model format or explicit openai flag
|
|
285
|
+
provider = if model_id.to_s.start_with?("openai/") || model_id.to_s.match?(/^gpt-/)
|
|
286
|
+
:openai
|
|
287
|
+
else
|
|
288
|
+
:openrouter
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
RubyLLM.chat(model: model_id, provider:, assume_model_exists: true)
|
|
292
|
+
end
|
|
283
293
|
end
|
|
284
294
|
|
|
285
295
|
# Dispatches a tool function call with the given function name and arguments.
|
|
@@ -307,42 +317,121 @@ module Raix
|
|
|
307
317
|
tools.select { |tool| requested_tools.include?(tool.dig(:function, :name).to_sym) }
|
|
308
318
|
end
|
|
309
319
|
|
|
310
|
-
def
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
end
|
|
320
|
+
def run_before_completion_hooks(params, messages)
|
|
321
|
+
hooks = [
|
|
322
|
+
Raix.configuration.before_completion,
|
|
323
|
+
self.class.configuration.before_completion,
|
|
324
|
+
before_completion
|
|
325
|
+
].compact
|
|
317
326
|
|
|
318
|
-
|
|
319
|
-
params[:stream_options] = { include_usage: true } if params[:stream]
|
|
327
|
+
return if hooks.empty?
|
|
320
328
|
|
|
321
|
-
|
|
329
|
+
context = CompletionContext.new(
|
|
330
|
+
chat_completion: self,
|
|
331
|
+
messages:,
|
|
332
|
+
params:
|
|
333
|
+
)
|
|
322
334
|
|
|
323
|
-
|
|
335
|
+
hooks.each do |hook|
|
|
336
|
+
result = hook.call(context) if hook.respond_to?(:call)
|
|
337
|
+
next unless result.is_a?(Hash)
|
|
338
|
+
|
|
339
|
+
# Handle model separately since it's passed as a keyword arg to ruby_llm_request
|
|
340
|
+
self.model = result[:model] if result.key?(:model)
|
|
341
|
+
params.merge!(result.compact)
|
|
342
|
+
end
|
|
324
343
|
end
|
|
325
344
|
|
|
326
|
-
def
|
|
327
|
-
#
|
|
328
|
-
|
|
345
|
+
def ruby_llm_request(params:, model:, messages:, openai_override: nil)
|
|
346
|
+
# Create a temporary chat instance for this request
|
|
347
|
+
provider = determine_provider(model, openai_override)
|
|
348
|
+
chat = RubyLLM.chat(model:, provider:, assume_model_exists: true)
|
|
349
|
+
|
|
350
|
+
# Apply messages to the chat
|
|
351
|
+
# Track if we have a user message to determine how to call ask
|
|
352
|
+
has_user_message = false
|
|
353
|
+
|
|
354
|
+
messages.each do |msg|
|
|
355
|
+
role = msg[:role] || msg["role"]
|
|
356
|
+
content = msg[:content] || msg["content"]
|
|
357
|
+
|
|
358
|
+
case role.to_s
|
|
359
|
+
when "system"
|
|
360
|
+
chat.with_instructions(content)
|
|
361
|
+
when "user"
|
|
362
|
+
has_user_message = true
|
|
363
|
+
chat.add_message(role: :user, content:)
|
|
364
|
+
when "assistant"
|
|
365
|
+
if msg[:tool_calls] || msg["tool_calls"]
|
|
366
|
+
chat.add_message(role: :assistant, content:, tool_calls: msg[:tool_calls] || msg["tool_calls"])
|
|
367
|
+
else
|
|
368
|
+
chat.add_message(role: :assistant, content:)
|
|
369
|
+
end
|
|
370
|
+
when "tool"
|
|
371
|
+
chat.add_message(
|
|
372
|
+
role: :tool,
|
|
373
|
+
content:,
|
|
374
|
+
tool_call_id: msg[:tool_call_id] || msg["tool_call_id"]
|
|
375
|
+
)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
329
378
|
|
|
330
|
-
|
|
379
|
+
# Apply configuration parameters
|
|
380
|
+
chat.with_temperature(params[:temperature]) if params[:temperature]
|
|
331
381
|
|
|
332
|
-
params
|
|
382
|
+
# Apply additional params (RubyLLM with_params expects keyword args)
|
|
383
|
+
additional_params = params.compact.except(:temperature, :tools, :max_tokens, :max_completion_tokens)
|
|
384
|
+
chat.with_params(**additional_params) if additional_params.any?
|
|
333
385
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
retry_count += 1
|
|
340
|
-
sleep 1 * retry_count # backoff
|
|
341
|
-
retry if retry_count < 5
|
|
342
|
-
end
|
|
386
|
+
# Handle tools - convert Raix function declarations to RubyLLM tools
|
|
387
|
+
if params[:tools].present? && respond_to?(:class) && self.class.respond_to?(:functions)
|
|
388
|
+
ruby_llm_tools = FunctionToolAdapter.convert_tools_for_ruby_llm(self)
|
|
389
|
+
ruby_llm_tools.each { |tool| chat.with_tool(tool) }
|
|
390
|
+
end
|
|
343
391
|
|
|
344
|
-
|
|
392
|
+
# Execute the completion
|
|
393
|
+
if stream.present?
|
|
394
|
+
# Streaming mode
|
|
395
|
+
if has_user_message
|
|
396
|
+
chat.complete(&stream)
|
|
397
|
+
else
|
|
398
|
+
chat.ask(&stream)
|
|
399
|
+
end
|
|
400
|
+
nil # Return nil for streaming as per original behavior
|
|
401
|
+
else
|
|
402
|
+
# Non-streaming mode - return OpenAI-compatible response format
|
|
403
|
+
response_message = has_user_message ? chat.complete : chat.ask
|
|
404
|
+
|
|
405
|
+
# Convert RubyLLM response to OpenAI format for compatibility
|
|
406
|
+
{
|
|
407
|
+
"choices" => [
|
|
408
|
+
{
|
|
409
|
+
"message" => {
|
|
410
|
+
"role" => "assistant",
|
|
411
|
+
"content" => response_message.content,
|
|
412
|
+
"tool_calls" => response_message.tool_calls
|
|
413
|
+
},
|
|
414
|
+
"finish_reason" => response_message.tool_call? ? "tool_calls" : "stop"
|
|
415
|
+
}
|
|
416
|
+
],
|
|
417
|
+
"usage" => {
|
|
418
|
+
"prompt_tokens" => response_message.input_tokens,
|
|
419
|
+
"completion_tokens" => response_message.output_tokens,
|
|
420
|
+
"total_tokens" => (response_message.input_tokens || 0) + (response_message.output_tokens || 0)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
345
423
|
end
|
|
424
|
+
rescue StandardError => e
|
|
425
|
+
warn "RubyLLM request failed: #{e.message}"
|
|
426
|
+
raise e
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def determine_provider(model, openai_override)
|
|
430
|
+
return :openai if openai_override
|
|
431
|
+
return :openai if model.to_s.match?(/^gpt-/) || model.to_s.match?(/^o\d/)
|
|
432
|
+
|
|
433
|
+
# Default to openrouter for model IDs with provider prefix
|
|
434
|
+
:openrouter
|
|
346
435
|
end
|
|
347
436
|
end
|
|
348
437
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raix
|
|
4
|
+
# Context object passed to before_completion hooks.
|
|
5
|
+
# Provides access to the chat completion instance, messages, and request parameters.
|
|
6
|
+
# Messages can be mutated for content filtering, PII redaction, etc.
|
|
7
|
+
class CompletionContext
|
|
8
|
+
attr_reader :chat_completion, :messages, :params
|
|
9
|
+
|
|
10
|
+
def initialize(chat_completion:, messages:, params:)
|
|
11
|
+
@chat_completion = chat_completion
|
|
12
|
+
@messages = messages # mutable - hooks can modify for filtering, redaction, etc.
|
|
13
|
+
@params = params # mutable - hooks can modify parameters
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Convenience accessor for the transcript
|
|
17
|
+
def transcript
|
|
18
|
+
chat_completion.transcript
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Get the currently configured model
|
|
22
|
+
def current_model
|
|
23
|
+
chat_completion.model || chat_completion.configuration.model
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Get the class that includes ChatCompletion
|
|
27
|
+
def chat_completion_class
|
|
28
|
+
chat_completion.class
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Get the current configuration
|
|
32
|
+
def configuration
|
|
33
|
+
chat_completion.configuration
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/raix/configuration.rb
CHANGED
|
@@ -30,16 +30,24 @@ module Raix
|
|
|
30
30
|
# is normally set in each class that includes the ChatCompletion module.
|
|
31
31
|
attr_accessor_with_fallback :model
|
|
32
32
|
|
|
33
|
-
#
|
|
33
|
+
# DEPRECATED: Use ruby_llm_config.openrouter_api_key instead
|
|
34
34
|
attr_accessor_with_fallback :openrouter_client
|
|
35
35
|
|
|
36
|
-
#
|
|
36
|
+
# DEPRECATED: Use ruby_llm_config.openai_api_key instead
|
|
37
37
|
attr_accessor_with_fallback :openai_client
|
|
38
38
|
|
|
39
39
|
# The max_tool_calls option determines the maximum number of tool calls
|
|
40
40
|
# before forcing a text response to prevent excessive function invocations.
|
|
41
41
|
attr_accessor_with_fallback :max_tool_calls
|
|
42
42
|
|
|
43
|
+
# Access to RubyLLM configuration
|
|
44
|
+
attr_accessor_with_fallback :ruby_llm_config
|
|
45
|
+
|
|
46
|
+
# A callable hook that runs before each chat completion request.
|
|
47
|
+
# Receives a CompletionContext and can modify params and messages.
|
|
48
|
+
# Use for: dynamic parameter resolution, logging, content filtering, PII redaction, etc.
|
|
49
|
+
attr_accessor_with_fallback :before_completion
|
|
50
|
+
|
|
43
51
|
DEFAULT_MAX_TOKENS = 1000
|
|
44
52
|
DEFAULT_MAX_COMPLETION_TOKENS = 16_384
|
|
45
53
|
DEFAULT_MODEL = "meta-llama/llama-3.3-8b-instruct:free"
|
|
@@ -53,11 +61,18 @@ module Raix
|
|
|
53
61
|
self.max_tokens = DEFAULT_MAX_TOKENS
|
|
54
62
|
self.model = DEFAULT_MODEL
|
|
55
63
|
self.max_tool_calls = DEFAULT_MAX_TOOL_CALLS
|
|
64
|
+
self.ruby_llm_config = RubyLLM.config
|
|
56
65
|
self.fallback = fallback
|
|
57
66
|
end
|
|
58
67
|
|
|
59
68
|
def client?
|
|
60
|
-
|
|
69
|
+
# Support legacy openrouter_client/openai_client or new RubyLLM config
|
|
70
|
+
!!(openrouter_client || openai_client || ruby_llm_configured?)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def ruby_llm_configured?
|
|
74
|
+
ruby_llm_config&.openai_api_key || ruby_llm_config&.openrouter_api_key ||
|
|
75
|
+
ruby_llm_config&.anthropic_api_key || ruby_llm_config&.gemini_api_key
|
|
61
76
|
end
|
|
62
77
|
|
|
63
78
|
private
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raix
|
|
4
|
+
# Adapter to convert Raix function declarations to RubyLLM::Tool instances
|
|
5
|
+
class FunctionToolAdapter
|
|
6
|
+
def self.create_tool_from_function(function_def, instance)
|
|
7
|
+
tool_class = Class.new(RubyLLM::Tool) do
|
|
8
|
+
description function_def[:description] if function_def[:description]
|
|
9
|
+
|
|
10
|
+
# Define parameters based on function definition
|
|
11
|
+
function_def[:parameters][:properties]&.each do |param_name, param_def|
|
|
12
|
+
required = function_def[:parameters][:required]&.include?(param_name)
|
|
13
|
+
param param_name.to_sym, type: param_def[:type], desc: param_def[:description], required:
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Store reference to the instance and function name
|
|
17
|
+
define_method(:raix_instance) { instance }
|
|
18
|
+
define_method(:raix_function_name) { function_def[:name] }
|
|
19
|
+
|
|
20
|
+
# Override execute to call the Raix function
|
|
21
|
+
define_method(:execute) do |**args|
|
|
22
|
+
raix_instance.public_send(raix_function_name, args.with_indifferent_access, nil)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Set a meaningful name for the tool class
|
|
27
|
+
tool_class.define_singleton_method(:name) do
|
|
28
|
+
"Raix::GeneratedTool::#{function_def[:name].to_s.camelize}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
tool_instance = tool_class.new
|
|
32
|
+
|
|
33
|
+
# Override the name method to return the original function name
|
|
34
|
+
# This ensures RubyLLM can match the tool call from the AI
|
|
35
|
+
tool_instance.define_singleton_method(:name) do
|
|
36
|
+
function_def[:name].to_s
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
tool_instance
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.convert_tools_for_ruby_llm(raix_instance)
|
|
43
|
+
return [] unless raix_instance.class.respond_to?(:functions)
|
|
44
|
+
return [] if raix_instance.class.functions.blank?
|
|
45
|
+
|
|
46
|
+
raix_instance.class.functions.map do |function_def|
|
|
47
|
+
create_tool_from_function(function_def, raix_instance)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raix
|
|
4
|
+
# Adapter to convert between Raix's transcript array format and RubyLLM's Message objects
|
|
5
|
+
class TranscriptAdapter
|
|
6
|
+
attr_reader :ruby_llm_chat
|
|
7
|
+
|
|
8
|
+
def initialize(ruby_llm_chat)
|
|
9
|
+
@ruby_llm_chat = ruby_llm_chat
|
|
10
|
+
@pending_messages = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Add a message in Raix format (hash) to the transcript
|
|
14
|
+
def <<(message_hash)
|
|
15
|
+
case message_hash
|
|
16
|
+
when Array
|
|
17
|
+
# Handle nested arrays (from function dispatch)
|
|
18
|
+
message_hash.each { |msg| self << msg }
|
|
19
|
+
when Hash
|
|
20
|
+
add_message_from_hash(message_hash)
|
|
21
|
+
end
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Return all messages in Raix-compatible format
|
|
26
|
+
def flatten
|
|
27
|
+
ruby_llm_messages = @ruby_llm_chat.messages.map { |msg| message_to_raix_format(msg) }
|
|
28
|
+
pending = @pending_messages.map { |msg| normalize_message_format(msg) }
|
|
29
|
+
(ruby_llm_messages + pending).flatten
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Get all messages including pending ones
|
|
33
|
+
def to_a
|
|
34
|
+
flatten
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Allow iteration
|
|
38
|
+
def compact
|
|
39
|
+
flatten.compact
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Clear all messages
|
|
43
|
+
def clear
|
|
44
|
+
@ruby_llm_chat.reset_messages!
|
|
45
|
+
@pending_messages.clear
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Get last message
|
|
50
|
+
def last
|
|
51
|
+
flatten.last
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get size of transcript
|
|
55
|
+
def size
|
|
56
|
+
flatten.size
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
alias length size
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def add_message_from_hash(hash)
|
|
64
|
+
# Raix abbreviated format: { system: "text" }, { user: "text" }, { assistant: "text" }
|
|
65
|
+
if hash.key?(:system) || hash.key?("system")
|
|
66
|
+
content = hash[:system] || hash["system"]
|
|
67
|
+
@ruby_llm_chat.with_instructions(content)
|
|
68
|
+
@pending_messages << { role: "system", content: }
|
|
69
|
+
elsif hash.key?(:user) || hash.key?("user")
|
|
70
|
+
content = hash[:user] || hash["user"]
|
|
71
|
+
# Don't add to ruby_llm_chat yet - wait for chat_completion call
|
|
72
|
+
@pending_messages << { role: "user", content: }
|
|
73
|
+
elsif hash.key?(:assistant) || hash.key?("assistant")
|
|
74
|
+
content = hash[:assistant] || hash["assistant"]
|
|
75
|
+
@pending_messages << { role: "assistant", content: }
|
|
76
|
+
elsif hash[:role] || hash["role"]
|
|
77
|
+
# Standard OpenAI format (tool messages, assistant with tool_calls, etc.)
|
|
78
|
+
@pending_messages << hash.with_indifferent_access
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def message_to_raix_format(message)
|
|
83
|
+
# Return in Raix abbreviated format { system: "...", user: "...", assistant: "..." }
|
|
84
|
+
# unless it's a tool message which needs full format
|
|
85
|
+
if message.tool_call? || message.tool_result?
|
|
86
|
+
result = {
|
|
87
|
+
role: message.role.to_s,
|
|
88
|
+
content: message.content
|
|
89
|
+
}
|
|
90
|
+
result[:tool_calls] = message.tool_calls if message.tool_call?
|
|
91
|
+
result[:tool_call_id] = message.tool_call_id if message.tool_result?
|
|
92
|
+
result
|
|
93
|
+
else
|
|
94
|
+
# Use abbreviated format
|
|
95
|
+
{ message.role.to_sym => message.content }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def normalize_message_format(msg)
|
|
100
|
+
# If already in abbreviated format, return as-is
|
|
101
|
+
return msg if msg.key?(:system) || msg.key?(:user) || msg.key?(:assistant)
|
|
102
|
+
return msg if msg["system"] || msg["user"] || msg["assistant"]
|
|
103
|
+
|
|
104
|
+
# If in standard format with role/content, convert to abbreviated
|
|
105
|
+
if msg[:role] || msg["role"]
|
|
106
|
+
role = (msg[:role] || msg["role"]).to_sym
|
|
107
|
+
content = msg[:content] || msg["content"]
|
|
108
|
+
|
|
109
|
+
# Tool messages stay in full format
|
|
110
|
+
if msg[:tool_calls] || msg["tool_calls"] || msg[:tool_call_id] || msg["tool_call_id"]
|
|
111
|
+
return msg
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Convert to abbreviated format
|
|
115
|
+
{ role => content }
|
|
116
|
+
else
|
|
117
|
+
msg
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
data/lib/raix/version.rb
CHANGED
data/lib/raix.rb
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require "ruby_llm"
|
|
4
|
+
|
|
5
|
+
require_relative "raix/completion_context"
|
|
4
6
|
require_relative "raix/configuration"
|
|
7
|
+
require_relative "raix/version"
|
|
8
|
+
require_relative "raix/transcript_adapter"
|
|
9
|
+
require_relative "raix/function_tool_adapter"
|
|
5
10
|
require_relative "raix/chat_completion"
|
|
6
11
|
require_relative "raix/function_dispatch"
|
|
7
12
|
require_relative "raix/prompt_declarations"
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: raix
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Obie Fernandez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2025-
|
|
10
|
+
date: 2025-12-17 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activesupport
|
|
@@ -37,20 +37,6 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '2.0'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: open_router
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - "~>"
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0.2'
|
|
47
|
-
type: :runtime
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - "~>"
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '0.2'
|
|
54
40
|
- !ruby/object:Gem::Dependency
|
|
55
41
|
name: ostruct
|
|
56
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -66,19 +52,19 @@ dependencies:
|
|
|
66
52
|
- !ruby/object:Gem::Version
|
|
67
53
|
version: '0'
|
|
68
54
|
- !ruby/object:Gem::Dependency
|
|
69
|
-
name:
|
|
55
|
+
name: ruby_llm
|
|
70
56
|
requirement: !ruby/object:Gem::Requirement
|
|
71
57
|
requirements:
|
|
72
58
|
- - "~>"
|
|
73
59
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: '
|
|
60
|
+
version: '1.9'
|
|
75
61
|
type: :runtime
|
|
76
62
|
prerelease: false
|
|
77
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
64
|
requirements:
|
|
79
65
|
- - "~>"
|
|
80
66
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: '
|
|
67
|
+
version: '1.9'
|
|
82
68
|
email:
|
|
83
69
|
- obiefernandez@gmail.com
|
|
84
70
|
executables: []
|
|
@@ -103,15 +89,17 @@ files:
|
|
|
103
89
|
- lib/mcp/tool.rb
|
|
104
90
|
- lib/raix.rb
|
|
105
91
|
- lib/raix/chat_completion.rb
|
|
92
|
+
- lib/raix/completion_context.rb
|
|
106
93
|
- lib/raix/configuration.rb
|
|
107
94
|
- lib/raix/function_dispatch.rb
|
|
95
|
+
- lib/raix/function_tool_adapter.rb
|
|
108
96
|
- lib/raix/mcp.rb
|
|
109
97
|
- lib/raix/message_adapters/base.rb
|
|
110
98
|
- lib/raix/predicate.rb
|
|
111
99
|
- lib/raix/prompt_declarations.rb
|
|
112
100
|
- lib/raix/response_format.rb
|
|
101
|
+
- lib/raix/transcript_adapter.rb
|
|
113
102
|
- lib/raix/version.rb
|
|
114
|
-
- raix.gemspec
|
|
115
103
|
- sig/raix.rbs
|
|
116
104
|
homepage: https://github.com/OlympiaAI/raix
|
|
117
105
|
licenses:
|
data/raix.gemspec
DELETED
|
@@ -1,36 +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 "ostruct"
|
|
35
|
-
spec.add_dependency "ruby-openai", "~> 8.1"
|
|
36
|
-
end
|