raix 0.4.1 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00be6de8258fbe226ea622d4d0e591fa6035ddd8d5ad627dd316291561317355
4
- data.tar.gz: 2749f23745e8385a770d1214c131c3babd2b48e2c983c10eaa1775ae85151879
3
+ metadata.gz: 65adb56d006baa2b25193eaa5f87f52b24366876139d1e023c9031e7707f62ea
4
+ data.tar.gz: 545da8c28e699f87e53547b53d07fb934727948945f45998f3af5d34752a2854
5
5
  SHA512:
6
- metadata.gz: 561520feed1ab27d400e1482ecc3a3740fdb3d21b0ac09603f59b1b798facf9d6bb83f910c4f77b66ce7b5a60d6278bbe174429853e74aa5e5ccda771603566e
7
- data.tar.gz: 537a7d27ad0d9a717f52bdb7a0aaa0d6f8479f0261f9730f18d0e93255c734e027d577d9284425a213d151fb08dd95c2f988a21015cd7746706e989dedb91cd0
6
+ metadata.gz: 71a14254199a7e0eeb8195b842fe212516091743ab4799f349caadfeffa654eede58dc066813e4d1360c24b2384406f934086d9b167b45db70a618470c94df84
7
+ data.tar.gz: 3d5a8033107bc8037862d11cd88fb3cd7a67a6884b4f7d2d8756244ac676e938094a6580bd55275b353c0b12389f292cbe958138d8d13f2d1001c5befb81662d
data/CHANGELOG.md CHANGED
@@ -15,3 +15,9 @@
15
15
  ## [0.4.0] - 2024-10-18
16
16
  - adds support for Anthropic-style prompt caching
17
17
  - defaults to `max_completion_tokens` when using OpenAI directly
18
+
19
+ ## [0.4.2] - 2024-11-05
20
+ - adds support for [Predicted Outputs](https://platform.openai.com/docs/guides/latency-optimization#use-predicted-outputs) with the `prediction` option for OpenAI
21
+
22
+ ## [0.4.3] - 2024-11-11
23
+ - adds support for `Predicate` module
data/Gemfile CHANGED
@@ -22,3 +22,8 @@ group :development do
22
22
  gem "sorbet"
23
23
  gem "tapioca", require: false
24
24
  end
25
+
26
+ group :test do
27
+ gem "vcr"
28
+ gem "webmock"
29
+ end
data/Gemfile.lock CHANGED
@@ -1,9 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- raix (0.4.1)
4
+ raix (0.4.3)
5
5
  activesupport (>= 6.0)
6
6
  open_router (~> 0.2)
7
+ ruby-openai (~> 7.0)
7
8
 
8
9
  GEM
9
10
  remote: https://rubygems.org/
@@ -18,6 +19,8 @@ GEM
18
19
  minitest (>= 5.1)
19
20
  mutex_m
20
21
  tzinfo (~> 2.0)
22
+ addressable (2.8.6)
23
+ public_suffix (>= 2.0.2, < 6.0)
21
24
  ast (2.4.2)
22
25
  backport (1.2.0)
23
26
  base64 (0.2.0)
@@ -26,6 +29,8 @@ GEM
26
29
  coderay (1.1.3)
27
30
  concurrent-ruby (1.3.3)
28
31
  connection_pool (2.4.1)
32
+ crack (0.4.5)
33
+ rexml
29
34
  diff-lcs (1.5.1)
30
35
  dotenv (3.1.2)
31
36
  drb (2.2.1)
@@ -57,6 +62,7 @@ GEM
57
62
  guard (~> 2.1)
58
63
  guard-compat (~> 1.1)
59
64
  rspec (>= 2.99.0, < 4.0)
65
+ hashdiff (1.0.1)
60
66
  i18n (1.14.5)
61
67
  concurrent-ruby (~> 1.0)
62
68
  jaro_winkler (1.6.0)
@@ -98,6 +104,7 @@ GEM
98
104
  pry (0.14.2)
99
105
  coderay (~> 1.1)
100
106
  method_source (~> 1.0)
107
+ public_suffix (5.0.5)
101
108
  racc (1.7.3)
102
109
  rainbow (3.1.1)
103
110
  rake (13.2.0)
@@ -191,6 +198,11 @@ GEM
191
198
  concurrent-ruby (~> 1.0)
192
199
  unicode-display_width (2.5.0)
193
200
  uri (0.13.0)
201
+ vcr (6.2.0)
202
+ webmock (3.18.1)
203
+ addressable (>= 2.8.0)
204
+ crack (>= 0.3.2)
205
+ hashdiff (>= 0.4.0, < 2.0.0)
194
206
  yard (0.9.36)
195
207
  yard-sorbet (0.8.1)
196
208
  sorbet-runtime (>= 0.5)
@@ -217,6 +229,8 @@ DEPENDENCIES
217
229
  solargraph-rails (~> 0.2.0.pre)
218
230
  sorbet
219
231
  tapioca
232
+ vcr
233
+ webmock
220
234
 
221
235
  BUNDLED WITH
222
236
  2.4.12
data/Guardfile ADDED
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A sample Guardfile
4
+ # More info at https://github.com/guard/guard#readme
5
+
6
+ ## Uncomment and set this to only include directories you want to watch
7
+ # directories %w(app lib config test spec features) \
8
+ # .select{|d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")}
9
+
10
+ ## Note: if you are using the `directories` clause above and you are not
11
+ ## watching the project directory ('.'), then you will want to move
12
+ ## the Guardfile to a watched dir and symlink it back, e.g.
13
+ #
14
+ # $ mkdir config
15
+ # $ mv Guardfile config/
16
+ # $ ln -s config/Guardfile .
17
+ #
18
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
19
+
20
+ # NOTE: The cmd option is now required due to the increasing number of ways
21
+ # rspec may be run, below are examples of the most common uses.
22
+ # * bundler: 'bundle exec rspec'
23
+ # * bundler binstubs: 'bin/rspec'
24
+ # * spring: 'bin/rspec' (This will use spring if running and you have
25
+ # installed the spring binstubs per the docs)
26
+ # * zeus: 'zeus rspec' (requires the server to be started separately)
27
+ # * 'just' rspec: 'rspec'
28
+
29
+ guard :rspec, cmd: "bundle exec rspec" do
30
+ require "guard/rspec/dsl"
31
+ dsl = Guard::RSpec::Dsl.new(self)
32
+
33
+ # Feel free to open issues for suggestions and improvements
34
+
35
+ # RSpec files
36
+ rspec = dsl.rspec
37
+ watch(rspec.spec_helper) { rspec.spec_dir }
38
+ watch(rspec.spec_support) { rspec.spec_dir }
39
+ watch(rspec.spec_files)
40
+
41
+ # Ruby files
42
+ ruby = dsl.ruby
43
+ dsl.watch_spec_files_for(ruby.lib_files)
44
+
45
+ # Rails files
46
+ rails = dsl.rails(view_extensions: %w[erb haml slim])
47
+ dsl.watch_spec_files_for(rails.app_files)
48
+ dsl.watch_spec_files_for(rails.views)
49
+
50
+ watch(rails.controllers) do |m|
51
+ [
52
+ rspec.spec.call("routing/#{m[1]}_routing"),
53
+ rspec.spec.call("controllers/#{m[1]}_controller"),
54
+ rspec.spec.call("acceptance/#{m[1]}")
55
+ ]
56
+ end
57
+
58
+ # Rails config changes
59
+ watch(rails.spec_helper) { rspec.spec_dir }
60
+ watch(rails.routes) { "#{rspec.spec_dir}/routing" }
61
+ watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
62
+
63
+ # Capybara features specs
64
+ watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
65
+ watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }
66
+
67
+ # Turnip features and steps
68
+ watch(%r{^spec/acceptance/(.+)\.feature$})
69
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
70
+ Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
71
+ end
72
+ end
data/README.md CHANGED
@@ -42,6 +42,14 @@ 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
+ ### Predicted Outputs
46
+
47
+ Raix supports [Predicted Outputs](https://platform.openai.com/docs/guides/latency-optimization#use-predicted-outputs) with the `prediction` parameter for OpenAI.
48
+
49
+ ```ruby
50
+ >> ai.chat_completion(openai: "gpt-4o", params: { prediction: })
51
+ ```
52
+
45
53
  ### Prompt Caching
46
54
 
47
55
  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".
@@ -230,6 +238,77 @@ Notably, Olympia does not use the `FunctionDispatch` module in its primary conve
230
238
 
231
239
  Streaming of the AI's response to the end user is handled by the `ReplyStream` class, passed to the final prompt declaration as its `stream` parameter. [Patterns of Application Development Using AI](https://leanpub.com/patterns-of-application-development-using-ai) devotes a whole chapter to describing how to write your own `ReplyStream` class.
232
240
 
241
+ ## Predicate Module
242
+
243
+ The `Raix::Predicate` module provides a simple way to handle yes/no/maybe questions using AI chat completion. It allows you to define blocks that handle different types of responses with their explanations. It is one of the concrete patterns described in the "Discrete Components" chapter of [Patterns of Application Development Using AI](https://leanpub.com/patterns-of-application-development-using-ai).
244
+
245
+ ### Usage
246
+
247
+ Include the `Raix::Predicate` module in your class and define handlers using block syntax:
248
+
249
+ ```ruby
250
+ class Question
251
+ include Raix::Predicate
252
+
253
+ yes? do |explanation|
254
+ puts "Affirmative: #{explanation}"
255
+ end
256
+
257
+ no? do |explanation|
258
+ puts "Negative: #{explanation}"
259
+ end
260
+
261
+ maybe? do |explanation|
262
+ puts "Uncertain: #{explanation}"
263
+ end
264
+ end
265
+
266
+ question = Question.new
267
+ question.ask("Is Ruby a programming language?")
268
+ # => Affirmative: Yes, Ruby is a dynamic, object-oriented programming language...
269
+ ```
270
+
271
+ ### Features
272
+
273
+ - Define handlers for yes, no, and/or maybe responses using the declarative class level block syntax.
274
+ - At least one handler (yes, no, or maybe) must be defined.
275
+ - Handlers receive the full AI response including explanation as an argument.
276
+ - Responses always start with "Yes, ", "No, ", or "Maybe, " followed by an explanation.
277
+ - Make sure to ask a question that can be answered with yes, no, or maybe (otherwise the results are indeterminate).
278
+
279
+ ### Example with Single Handler
280
+
281
+ You can define only the handlers you need:
282
+
283
+ ```ruby
284
+ class SimpleQuestion
285
+ include Raix::Predicate
286
+
287
+ # Only handle positive responses
288
+ yes? do |explanation|
289
+ puts "✅ #{explanation}"
290
+ end
291
+ end
292
+
293
+ question = SimpleQuestion.new
294
+ question.ask("Is 2 + 2 = 4?")
295
+ # => ✅ Yes, 2 + 2 equals 4, this is a fundamental mathematical fact.
296
+ ```
297
+
298
+ ### Error Handling
299
+
300
+ The module will raise a RuntimeError if you attempt to ask a question without defining any response handlers:
301
+
302
+ ```ruby
303
+ class InvalidQuestion
304
+ include Raix::Predicate
305
+ end
306
+
307
+ question = InvalidQuestion.new
308
+ question.ask("Any question")
309
+ # => RuntimeError: Please define a yes and/or no block
310
+ ```
311
+
233
312
  ## Installation
234
313
 
235
314
  Install the gem and add to the application's Gemfile by executing:
@@ -20,7 +20,7 @@ module Raix
20
20
  extend ActiveSupport::Concern
21
21
 
22
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,
23
+ :prediction, :repetition_penalty, :response_format, :stream, :temperature, :max_completion_tokens,
24
24
  :max_tokens, :seed, :stop, :top_a, :top_k, :top_logprobs, :top_p, :tools, :tool_choice, :provider
25
25
 
26
26
  # This method performs chat completion based on the provided transcript and parameters.
@@ -40,6 +40,7 @@ module Raix
40
40
  params[:max_completion_tokens] ||= max_completion_tokens.presence || Raix.configuration.max_completion_tokens
41
41
  params[:max_tokens] ||= max_tokens.presence || Raix.configuration.max_tokens
42
42
  params[:min_p] ||= min_p.presence
43
+ params[:prediction] = { type: "content", content: params[:prediction] || prediction } if params[:prediction] || prediction.present?
43
44
  params[:presence_penalty] ||= presence_penalty.presence
44
45
  params[:provider] ||= provider.presence
45
46
  params[:repetition_penalty] ||= repetition_penalty.presence
@@ -150,8 +151,12 @@ module Raix
150
151
  private
151
152
 
152
153
  def openai_request(params:, model:, messages:)
153
- # deprecated in favor of max_completion_tokens
154
- params.delete(:max_tokens)
154
+ if params[:prediction]
155
+ params.delete(:max_completion_tokens)
156
+ else
157
+ params[:max_completion_tokens] ||= params[:max_tokens]
158
+ params.delete(:max_tokens)
159
+ end
155
160
 
156
161
  params[:stream] ||= stream.presence
157
162
  params[:stream_options] = { include_usage: true } if params[:stream]
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raix
4
+ # A module for handling yes/no questions using AI chat completion.
5
+ # When included in a class, it provides methods to define handlers for
6
+ # yes and no responses.
7
+ #
8
+ # @example
9
+ # class Question
10
+ # include Raix::Predicate
11
+ #
12
+ # yes do |explanation|
13
+ # puts "Yes: #{explanation}"
14
+ # end
15
+ #
16
+ # no do |explanation|
17
+ # puts "No: #{explanation}"
18
+ # end
19
+ # end
20
+ #
21
+ # question = Question.new
22
+ # question.ask("Is Ruby a programming language?")
23
+ module Predicate
24
+ include ChatCompletion
25
+
26
+ def self.included(base)
27
+ base.extend(ClassMethods)
28
+ end
29
+
30
+ def ask(question)
31
+ raise "Please define a yes and/or no block" if self.class.yes_block.nil? && self.class.no_block.nil?
32
+
33
+ transcript << { system: "Always answer 'Yes, ', 'No, ', or 'Maybe, ' followed by a concise explanation!" }
34
+ transcript << { user: question }
35
+
36
+ chat_completion.tap do |response|
37
+ if response.downcase.start_with?("yes,")
38
+ instance_exec(response, &self.class.yes_block) if self.class.yes_block
39
+ elsif response.downcase.start_with?("no,")
40
+ instance_exec(response, &self.class.no_block) if self.class.no_block
41
+ elsif response.downcase.start_with?("maybe,")
42
+ instance_exec(response, &self.class.maybe_block) if self.class.maybe_block
43
+ end
44
+ end
45
+ end
46
+
47
+ # Class methods added to the including class
48
+ module ClassMethods
49
+ attr_reader :yes_block, :no_block, :maybe_block
50
+
51
+ def yes?(&block)
52
+ @yes_block = block
53
+ end
54
+
55
+ def no?(&block)
56
+ @no_block = block
57
+ end
58
+
59
+ def maybe?(&block)
60
+ @maybe_block = block
61
+ end
62
+ end
63
+ end
64
+ 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.4.1"
4
+ VERSION = "0.4.3"
5
5
  end
data/raix.gemspec CHANGED
@@ -30,4 +30,5 @@ Gem::Specification.new do |spec|
30
30
 
31
31
  spec.add_dependency "activesupport", ">= 6.0"
32
32
  spec.add_dependency "open_router", "~> 0.2"
33
+ spec.add_dependency "ruby-openai", "~> 7.0"
33
34
  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.1
4
+ version: 0.4.3
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-10-22 00:00:00.000000000 Z
11
+ date: 2024-11-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: ruby-openai
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '7.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '7.0'
41
55
  description:
42
56
  email:
43
57
  - obiefernandez@gmail.com
@@ -52,6 +66,7 @@ files:
52
66
  - CODE_OF_CONDUCT.md
53
67
  - Gemfile
54
68
  - Gemfile.lock
69
+ - Guardfile
55
70
  - LICENSE.txt
56
71
  - README.md
57
72
  - Rakefile
@@ -59,6 +74,7 @@ files:
59
74
  - lib/raix/chat_completion.rb
60
75
  - lib/raix/function_dispatch.rb
61
76
  - lib/raix/message_adapters/base.rb
77
+ - lib/raix/predicate.rb
62
78
  - lib/raix/prompt_declarations.rb
63
79
  - lib/raix/response_format.rb
64
80
  - lib/raix/version.rb
@@ -86,7 +102,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
102
  - !ruby/object:Gem::Version
87
103
  version: '0'
88
104
  requirements: []
89
- rubygems_version: 3.5.21
105
+ rubygems_version: 3.4.10
90
106
  signing_key:
91
107
  specification_version: 4
92
108
  summary: Ruby AI eXtensions