raix 0.3.2 → 0.4.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 +1 -1
- data/CHANGELOG.md +7 -0
- data/Gemfile.lock +2 -1
- data/README.md +36 -0
- data/lib/raix/chat_completion.rb +35 -30
- data/lib/raix/function_dispatch.rb +1 -1
- data/lib/raix/message_adapters/base.rb +50 -0
- data/lib/raix/prompt_declarations.rb +1 -1
- data/lib/raix/response_format.rb +76 -0
- data/lib/raix/version.rb +1 -1
- data/lib/raix.rb +6 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 00be6de8258fbe226ea622d4d0e591fa6035ddd8d5ad627dd316291561317355
|
4
|
+
data.tar.gz: 2749f23745e8385a770d1214c131c3babd2b48e2c983c10eaa1775ae85151879
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 561520feed1ab27d400e1482ecc3a3740fdb3d21b0ac09603f59b1b798facf9d6bb83f910c4f77b66ce7b5a60d6278bbe174429853e74aa5e5ccda771603566e
|
7
|
+
data.tar.gz: 537a7d27ad0d9a717f52bdb7a0aaa0d6f8479f0261f9730f18d0e93255c734e027d577d9284425a213d151fb08dd95c2f988a21015cd7746706e989dedb91cd0
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -8,3 +8,10 @@
|
|
8
8
|
- adds `ChatCompletion` module
|
9
9
|
- adds `PromptDeclarations` module
|
10
10
|
- adds `FunctionDispatch` module
|
11
|
+
|
12
|
+
## [0.3.2] - 2024-06-29
|
13
|
+
- adds support for streaming
|
14
|
+
|
15
|
+
## [0.4.0] - 2024-10-18
|
16
|
+
- adds support for Anthropic-style prompt caching
|
17
|
+
- defaults to `max_completion_tokens` when using OpenAI directly
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -42,6 +42,30 @@ transcript << { role: "user", content: "What is the meaning of life?" }
|
|
42
42
|
|
43
43
|
One of the advantages of OpenRouter and the reason that it is used by default by this library is that it handles mapping message formats from the OpenAI standard to whatever other model you're wanting to use (Anthropic, Cohere, etc.)
|
44
44
|
|
45
|
+
### Prompt Caching
|
46
|
+
|
47
|
+
Raix supports [Anthropic-style prompt caching](https://openrouter.ai/docs/prompt-caching#anthropic-claude) when using Anthropic's Claud family of models. You can specify a `cache_at` parameter when doing a chat completion. If the character count for the content of a particular message is longer than the cache_at parameter, it will be sent to Anthropic as a multipart message with a cache control "breakpoint" set to "ephemeral".
|
48
|
+
|
49
|
+
Note that there is a limit of four breakpoints, and the cache will expire within five minutes. Therefore, it is recommended to reserve the cache breakpoints for large bodies of text, such as character cards, CSV data, RAG data, book chapters, etc. Raix does not enforce a limit on the number of breakpoints, which means that you might get an error if you try to cache too many messages.
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
>> my_class.chat_completion(params: { cache_at: 1000 })
|
53
|
+
=> {
|
54
|
+
"messages": [
|
55
|
+
{
|
56
|
+
"role": "system",
|
57
|
+
"content": [
|
58
|
+
{
|
59
|
+
"type": "text",
|
60
|
+
"text": "HUGE TEXT BODY LONGER THAN 1000 CHARACTERS",
|
61
|
+
"cache_control": {
|
62
|
+
"type": "ephemeral"
|
63
|
+
}
|
64
|
+
}
|
65
|
+
]
|
66
|
+
},
|
67
|
+
```
|
68
|
+
|
45
69
|
### Use of Tools/Functions
|
46
70
|
|
47
71
|
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 as part of a chat completion "loop" in a declarative, Rails-like "DSL" fashion.
|
@@ -216,6 +240,18 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
216
240
|
|
217
241
|
$ gem install raix
|
218
242
|
|
243
|
+
If you are using the default OpenRouter API, Raix expects `Raix.configuration.openrouter_client` to initialized with the OpenRouter API client instance.
|
244
|
+
|
245
|
+
You can add an initializer to your application's `config/initializers` directory:
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
# config/initializers/raix.rb
|
249
|
+
Raix.configure do |config|
|
250
|
+
config.openrouter_client = OpenRouter::Client.new
|
251
|
+
end
|
252
|
+
```
|
253
|
+
|
254
|
+
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
|
219
255
|
|
220
256
|
## Development
|
221
257
|
|
data/lib/raix/chat_completion.rb
CHANGED
@@ -5,6 +5,8 @@ require "active_support/core_ext/object/blank"
|
|
5
5
|
require "open_router"
|
6
6
|
require "openai"
|
7
7
|
|
8
|
+
require_relative "message_adapters/base"
|
9
|
+
|
8
10
|
module Raix
|
9
11
|
# The `ChatCompletion`` module is a Rails concern that provides a way to interact
|
10
12
|
# with the OpenRouter Chat Completion API via its client. The module includes a few
|
@@ -17,9 +19,9 @@ module Raix
|
|
17
19
|
module ChatCompletion
|
18
20
|
extend ActiveSupport::Concern
|
19
21
|
|
20
|
-
attr_accessor :frequency_penalty, :logit_bias, :logprobs, :loop, :min_p, :model, :presence_penalty,
|
21
|
-
:repetition_penalty, :response_format, :stream, :temperature, :
|
22
|
-
:top_k, :top_logprobs, :top_p, :tools, :tool_choice, :provider
|
22
|
+
attr_accessor :cache_at, :frequency_penalty, :logit_bias, :logprobs, :loop, :min_p, :model, :presence_penalty,
|
23
|
+
:repetition_penalty, :response_format, :stream, :temperature, :max_completion_tokens,
|
24
|
+
:max_tokens, :seed, :stop, :top_a, :top_k, :top_logprobs, :top_p, :tools, :tool_choice, :provider
|
23
25
|
|
24
26
|
# This method performs chat completion based on the provided transcript and parameters.
|
25
27
|
#
|
@@ -30,16 +32,12 @@ module Raix
|
|
30
32
|
# @option params [Boolean] :raw (false) Whether to return the raw response or dig the text content.
|
31
33
|
# @return [String|Hash] The completed chat response.
|
32
34
|
def chat_completion(params: {}, loop: false, json: false, raw: false, openai: false)
|
33
|
-
messages = transcript.flatten.compact.map { |msg| transform_message_format(msg) }
|
34
|
-
raise "Can't complete an empty transcript" if messages.blank?
|
35
|
-
|
36
|
-
# used by FunctionDispatch
|
37
|
-
self.loop = loop
|
38
|
-
|
39
35
|
# set params to default values if not provided
|
36
|
+
params[:cache_at] ||= cache_at.presence
|
40
37
|
params[:frequency_penalty] ||= frequency_penalty.presence
|
41
38
|
params[:logit_bias] ||= logit_bias.presence
|
42
39
|
params[:logprobs] ||= logprobs.presence
|
40
|
+
params[:max_completion_tokens] ||= max_completion_tokens.presence || Raix.configuration.max_completion_tokens
|
43
41
|
params[:max_tokens] ||= max_tokens.presence || Raix.configuration.max_tokens
|
44
42
|
params[:min_p] ||= min_p.presence
|
45
43
|
params[:presence_penalty] ||= presence_penalty.presence
|
@@ -57,23 +55,29 @@ module Raix
|
|
57
55
|
params[:top_p] ||= top_p.presence
|
58
56
|
|
59
57
|
if json
|
60
|
-
|
61
|
-
|
58
|
+
unless openai
|
59
|
+
params[:provider] ||= {}
|
60
|
+
params[:provider][:require_parameters] = true
|
61
|
+
end
|
62
62
|
params[:response_format] ||= {}
|
63
63
|
params[:response_format][:type] = "json_object"
|
64
64
|
end
|
65
65
|
|
66
|
+
# used by FunctionDispatch
|
67
|
+
self.loop = loop
|
68
|
+
|
66
69
|
# set the model to the default if not provided
|
67
70
|
self.model ||= Raix.configuration.model
|
68
71
|
|
72
|
+
adapter = MessageAdapters::Base.new(self)
|
73
|
+
messages = transcript.flatten.compact.map { |msg| adapter.transform(msg) }
|
74
|
+
raise "Can't complete an empty transcript" if messages.blank?
|
75
|
+
|
69
76
|
begin
|
70
77
|
response = if openai
|
71
|
-
openai_request(params:, model: openai,
|
72
|
-
messages:)
|
78
|
+
openai_request(params:, model: openai, messages:)
|
73
79
|
else
|
74
|
-
openrouter_request(
|
75
|
-
params:, model:, messages:
|
76
|
-
)
|
80
|
+
openrouter_request(params:, model:, messages:)
|
77
81
|
end
|
78
82
|
retry_count = 0
|
79
83
|
content = nil
|
@@ -115,8 +119,8 @@ module Raix
|
|
115
119
|
raise e # just fail if we can't get content after 3 attempts
|
116
120
|
end
|
117
121
|
|
118
|
-
|
119
|
-
|
122
|
+
puts "Bad JSON received!!!!!!: #{content}"
|
123
|
+
raise e
|
120
124
|
rescue Faraday::BadRequestError => e
|
121
125
|
# make sure we see the actual error message on console or Honeybadger
|
122
126
|
puts "Chat completion failed!!!!!!!!!!!!!!!!: #{e.response[:body]}"
|
@@ -132,6 +136,9 @@ module Raix
|
|
132
136
|
# { user: "Hey what time is it?" },
|
133
137
|
# { assistant: "Sorry, pumpkins do not wear watches" }
|
134
138
|
#
|
139
|
+
# to add a function call use the following format:
|
140
|
+
# { function: { name: 'fancy_pants_function', arguments: { param: 'value' } } }
|
141
|
+
#
|
135
142
|
# to add a function result use the following format:
|
136
143
|
# { function: result, name: 'fancy_pants_function' }
|
137
144
|
#
|
@@ -143,11 +150,21 @@ module Raix
|
|
143
150
|
private
|
144
151
|
|
145
152
|
def openai_request(params:, model:, messages:)
|
153
|
+
# deprecated in favor of max_completion_tokens
|
154
|
+
params.delete(:max_tokens)
|
155
|
+
|
146
156
|
params[:stream] ||= stream.presence
|
157
|
+
params[:stream_options] = { include_usage: true } if params[:stream]
|
158
|
+
|
159
|
+
params.delete(:temperature) if model == "o1-preview"
|
160
|
+
|
147
161
|
Raix.configuration.openai_client.chat(parameters: params.compact.merge(model:, messages:))
|
148
162
|
end
|
149
163
|
|
150
164
|
def openrouter_request(params:, model:, messages:)
|
165
|
+
# max_completion_tokens is not supported by OpenRouter
|
166
|
+
params.delete(:max_completion_tokens)
|
167
|
+
|
151
168
|
retry_count = 0
|
152
169
|
|
153
170
|
begin
|
@@ -163,17 +180,5 @@ module Raix
|
|
163
180
|
raise e
|
164
181
|
end
|
165
182
|
end
|
166
|
-
|
167
|
-
def transform_message_format(message)
|
168
|
-
return message if message[:role].present?
|
169
|
-
|
170
|
-
if message[:function].present?
|
171
|
-
{ role: "assistant", name: message.dig(:function, :name), content: message.dig(:function, :arguments).to_json }
|
172
|
-
elsif message[:result].present?
|
173
|
-
{ role: "function", name: message[:name], content: message[:result] }
|
174
|
-
else
|
175
|
-
{ role: message.first.first, content: message.first.last }
|
176
|
-
end
|
177
|
-
end
|
178
183
|
end
|
179
184
|
end
|
@@ -35,7 +35,7 @@ module Raix
|
|
35
35
|
# argument will be executed in the instance context of the class that includes this module.
|
36
36
|
#
|
37
37
|
# Example:
|
38
|
-
# function :google_search,
|
38
|
+
# function :google_search, "Search Google for something", query: { type: "string" } do |arguments|
|
39
39
|
# GoogleSearch.new(arguments[:query]).search
|
40
40
|
# end
|
41
41
|
#
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/module/delegation"
|
4
|
+
|
5
|
+
module Raix
|
6
|
+
module MessageAdapters
|
7
|
+
# Transforms messages into the format expected by the OpenAI API
|
8
|
+
class Base
|
9
|
+
attr_accessor :context
|
10
|
+
|
11
|
+
delegate :cache_at, :model, to: :context
|
12
|
+
|
13
|
+
def initialize(context)
|
14
|
+
@context = context
|
15
|
+
end
|
16
|
+
|
17
|
+
def transform(message)
|
18
|
+
return message if message[:role].present?
|
19
|
+
|
20
|
+
if message[:function].present?
|
21
|
+
{ role: "assistant", name: message.dig(:function, :name), content: message.dig(:function, :arguments).to_json }
|
22
|
+
elsif message[:result].present?
|
23
|
+
{ role: "function", name: message[:name], content: message[:result] }
|
24
|
+
else
|
25
|
+
content(message)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def content(message)
|
32
|
+
case message
|
33
|
+
in { system: content }
|
34
|
+
{ role: "system", content: }
|
35
|
+
in { user: content }
|
36
|
+
{ role: "user", content: }
|
37
|
+
in { assistant: content }
|
38
|
+
{ role: "assistant", content: }
|
39
|
+
else
|
40
|
+
raise ArgumentError, "Invalid message format: #{message.inspect}"
|
41
|
+
end.tap do |msg|
|
42
|
+
# convert to anthropic multipart format if model is claude-3 and cache_at is set
|
43
|
+
if model.to_s.include?("anthropic/claude-3") && cache_at && msg[:content].to_s.length > cache_at.to_i
|
44
|
+
msg[:content] = [{ type: "text", text: msg[:content], cache_control: { type: "ephemeral" } }]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -83,7 +83,7 @@ module Raix
|
|
83
83
|
params = @current_prompt.params.merge(params)
|
84
84
|
|
85
85
|
# set the stream if necessary
|
86
|
-
self.stream = instance_exec(
|
86
|
+
self.stream = instance_exec(&@current_prompt.stream) if @current_prompt.stream.present?
|
87
87
|
|
88
88
|
super(params:, raw:).then do |response|
|
89
89
|
transcript << { assistant: response }
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/object/deep_dup"
|
4
|
+
|
5
|
+
module Raix
|
6
|
+
# Handles the formatting of responses for AI interactions.
|
7
|
+
#
|
8
|
+
# This class is responsible for converting input data into a JSON schema
|
9
|
+
# that can be used to structure and validate AI responses. It supports
|
10
|
+
# nested structures and arrays, ensuring that the output conforms to
|
11
|
+
# the expected format for AI model interactions.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# input = { name: { type: "string" }, age: { type: "integer" } }
|
15
|
+
# format = ResponseFormat.new("PersonInfo", input)
|
16
|
+
# schema = format.to_schema
|
17
|
+
#
|
18
|
+
# @attr_reader [String] name The name of the response format
|
19
|
+
# @attr_reader [Hash] input The input data to be formatted
|
20
|
+
class ResponseFormat
|
21
|
+
def initialize(name, input)
|
22
|
+
@name = name
|
23
|
+
@input = input
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_json(*)
|
27
|
+
JSON.pretty_generate(to_schema)
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_schema
|
31
|
+
{
|
32
|
+
type: "json_schema",
|
33
|
+
json_schema: {
|
34
|
+
name: @name,
|
35
|
+
schema: {
|
36
|
+
type: "object",
|
37
|
+
properties: decode(@input.deep_dup),
|
38
|
+
required: @input.keys,
|
39
|
+
additionalProperties: false
|
40
|
+
},
|
41
|
+
strict: true
|
42
|
+
}
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def decode(input)
|
49
|
+
{}.tap do |response|
|
50
|
+
case input
|
51
|
+
when Array
|
52
|
+
properties = {}
|
53
|
+
input.each { |item| properties.merge!(decode(item)) }
|
54
|
+
|
55
|
+
response[:type] = "array"
|
56
|
+
response[:items] = {
|
57
|
+
type: "object",
|
58
|
+
properties:,
|
59
|
+
required: properties.keys.select { |key| properties[key].delete(:required) },
|
60
|
+
additionalProperties: false
|
61
|
+
}
|
62
|
+
when Hash
|
63
|
+
input.each do |key, value|
|
64
|
+
response[key] = if value.is_a?(Hash) && value.key?(:type)
|
65
|
+
value
|
66
|
+
else
|
67
|
+
decode(value)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
else
|
71
|
+
raise "Invalid input"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/lib/raix/version.rb
CHANGED
data/lib/raix.rb
CHANGED
@@ -4,6 +4,7 @@ require_relative "raix/version"
|
|
4
4
|
require_relative "raix/chat_completion"
|
5
5
|
require_relative "raix/function_dispatch"
|
6
6
|
require_relative "raix/prompt_declarations"
|
7
|
+
require_relative "raix/response_format"
|
7
8
|
|
8
9
|
# The Raix module provides configuration options for the Raix gem.
|
9
10
|
module Raix
|
@@ -16,6 +17,9 @@ module Raix
|
|
16
17
|
# The max_tokens option determines the maximum number of tokens to generate.
|
17
18
|
attr_accessor :max_tokens
|
18
19
|
|
20
|
+
# The max_completion_tokens option determines the maximum number of tokens to generate.
|
21
|
+
attr_accessor :max_completion_tokens
|
22
|
+
|
19
23
|
# The model option determines the model to use for text generation. This option
|
20
24
|
# is normally set in each class that includes the ChatCompletion module.
|
21
25
|
attr_accessor :model
|
@@ -27,12 +31,14 @@ module Raix
|
|
27
31
|
attr_accessor :openai_client
|
28
32
|
|
29
33
|
DEFAULT_MAX_TOKENS = 1000
|
34
|
+
DEFAULT_MAX_COMPLETION_TOKENS = 16_384
|
30
35
|
DEFAULT_MODEL = "meta-llama/llama-3-8b-instruct:free"
|
31
36
|
DEFAULT_TEMPERATURE = 0.0
|
32
37
|
|
33
38
|
# Initializes a new instance of the Configuration class with default values.
|
34
39
|
def initialize
|
35
40
|
self.temperature = DEFAULT_TEMPERATURE
|
41
|
+
self.max_completion_tokens = DEFAULT_MAX_COMPLETION_TOKENS
|
36
42
|
self.max_tokens = DEFAULT_MAX_TOKENS
|
37
43
|
self.model = DEFAULT_MODEL
|
38
44
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: raix
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Obie Fernandez
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-10-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -58,7 +58,9 @@ files:
|
|
58
58
|
- lib/raix.rb
|
59
59
|
- lib/raix/chat_completion.rb
|
60
60
|
- lib/raix/function_dispatch.rb
|
61
|
+
- lib/raix/message_adapters/base.rb
|
61
62
|
- lib/raix/prompt_declarations.rb
|
63
|
+
- lib/raix/response_format.rb
|
62
64
|
- lib/raix/version.rb
|
63
65
|
- raix.gemspec
|
64
66
|
- sig/raix.rbs
|
@@ -84,7 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
86
|
- !ruby/object:Gem::Version
|
85
87
|
version: '0'
|
86
88
|
requirements: []
|
87
|
-
rubygems_version: 3.
|
89
|
+
rubygems_version: 3.5.21
|
88
90
|
signing_key:
|
89
91
|
specification_version: 4
|
90
92
|
summary: Ruby AI eXtensions
|