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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29e66d0046995dca8f9d093ccfd719e54905f94c62d5ee4e78588e1b40b0dadd
4
- data.tar.gz: 7b1d4544576891124e209130d339be55fe35573e05dcb05474c526655e850390
3
+ metadata.gz: 00be6de8258fbe226ea622d4d0e591fa6035ddd8d5ad627dd316291561317355
4
+ data.tar.gz: 2749f23745e8385a770d1214c131c3babd2b48e2c983c10eaa1775ae85151879
5
5
  SHA512:
6
- metadata.gz: a8989d6e4c4422054a2e452637ffc3205a7242a060f32c86fcf5c72ff1050f50d7bc6b20a9e4eb147398cdbc461c6fe80cf1f1426e0401fe944ce484ae70d4f9
7
- data.tar.gz: 364ba4571799b8a7eea4abae9f4a905a32121303f62e22e66bff9f4663831b6311efa3eb9238f4eaa326456c2db467d7de0277000e42a11db710435c3e482679
6
+ metadata.gz: 561520feed1ab27d400e1482ecc3a3740fdb3d21b0ac09603f59b1b798facf9d6bb83f910c4f77b66ce7b5a60d6278bbe174429853e74aa5e5ccda771603566e
7
+ data.tar.gz: 537a7d27ad0d9a717f52bdb7a0aaa0d6f8479f0261f9730f18d0e93255c734e027d577d9284425a213d151fb08dd95c2f988a21015cd7746706e989dedb91cd0
data/.rubocop.yml CHANGED
@@ -11,7 +11,7 @@ Style/StringLiteralsInInterpolation:
11
11
  EnforcedStyle: double_quotes
12
12
 
13
13
  Layout/LineLength:
14
- Max: 120
14
+ Max: 180
15
15
 
16
16
  Metrics/BlockLength:
17
17
  Enabled: false
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- raix (0.3.1)
4
+ raix (0.4.1)
5
5
  activesupport (>= 6.0)
6
6
  open_router (~> 0.2)
7
7
 
@@ -198,6 +198,7 @@ GEM
198
198
 
199
199
  PLATFORMS
200
200
  arm64-darwin-21
201
+ arm64-darwin-22
201
202
  x86_64-linux
202
203
 
203
204
  DEPENDENCIES
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
 
@@ -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, :max_tokens, :seed, :stop, :top_a,
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
- params[:provider] ||= {}
61
- params[:provider][:require_parameters] = true
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
- # attempt to fix the JSON
119
- JsonFixer.new.call(content, e.message)
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, description: "Search Google for something", query: { type: "string" } do |arguments|
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(&current_prompt.stream) if current_prompt.stream.present?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Raix
4
- VERSION = "0.3.2"
4
+ VERSION = "0.4.1"
5
5
  end
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.3.2
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-06-30 00:00:00.000000000 Z
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.4.10
89
+ rubygems_version: 3.5.21
88
90
  signing_key:
89
91
  specification_version: 4
90
92
  summary: Ruby AI eXtensions