raix 0.4.2 → 0.4.4

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: cd88f295667264948d2710fb7eb0bcd74e5ab0177e76678314c34958e79fa6bd
4
- data.tar.gz: b86495b4b67c5259915a7ef20502005d90ededf6be991821703d1d83e876c572
3
+ metadata.gz: 50d0de4d7ec7fdd83776e539dbed2cc73ba3097c96752050eb768163dd5f510a
4
+ data.tar.gz: c82955632f789a0683e30553ecc8aef8b2f96aa4bc003e0b2e12953e6698adf9
5
5
  SHA512:
6
- metadata.gz: cf1982b065312860c046a363486169a3d4572b65bd5ded3e82e270e3cbb689a173d6ff94cbcc897d6d8e5f27ef7e6558193ea3b266c18cc5bd1a8f2ca54fa6cd
7
- data.tar.gz: 58eb7f54eb7d3a2656dbdd3e44a977c04b1dfb5ef5f7bd549ece96ecce9a931007edea5d364f189a1916883838c6a674e794799365914a1a5cab2402994da5f1
6
+ metadata.gz: 48977e0c2265105f22da2c1508a1aced37337ab1a914c7297bca4995cc94d6839708a51edb9f63222e4531bc0dccce305448ad70f78a858f30d7f581ae289114
7
+ data.tar.gz: c52f4078787b2570a6a11eb92e0a0f6172a09e083219d0b8562349fee4efc14012674e25591ef2839e41fd2675bfc834361bf1a64d3b74614854ce944b6cf8ab
data/CHANGELOG.md CHANGED
@@ -18,3 +18,6 @@
18
18
 
19
19
  ## [0.4.2] - 2024-11-05
20
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.2)
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
@@ -238,6 +238,77 @@ Notably, Olympia does not use the `FunctionDispatch` module in its primary conve
238
238
 
239
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.
240
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
+
241
312
  ## Installation
242
313
 
243
314
  Install the gem and add to the application's Gemfile by executing:
@@ -12,10 +12,22 @@ module Raix
12
12
  # with the OpenRouter Chat Completion API via its client. The module includes a few
13
13
  # methods that allow you to build a transcript of messages and then send them to
14
14
  # the API for completion. The API will return a response that you can use however
15
- # you see fit. If the response includes a function call, the module will dispatch
16
- # the function call and return the result. Which implies that function calls need
17
- # to be defined on the class that includes this module. (Note: You should probably
18
- # use the `FunctionDispatch` module to define functions instead of doing it manually.)
15
+ # you see fit.
16
+ #
17
+ # If the response includes a function call, the module will dispatch the function
18
+ # call and return the result. Which implies that function calls need to be defined
19
+ # on the class that includes this module. The `FunctionDispatch` module provides a
20
+ # Rails-like DSL for declaring and implementing tool functions at the top of your
21
+ # class instead of having to manually implement them as instance methods. The
22
+ # primary benefit of using the `FunctionDispatch` module is that it handles
23
+ # adding the function call results to the ongoing conversation transcript for you.
24
+ # It also triggers a new chat completion automatically if you've set the `loop`
25
+ # option to `true`, which is useful for implementing conversational chatbots that
26
+ # include tool calls.
27
+ #
28
+ # Note that some AI models can make more than a single tool function call in a
29
+ # single response. When that happens, the module will dispatch all of the function
30
+ # calls sequentially and return an array of results.
19
31
  module ChatCompletion
20
32
  extend ActiveSupport::Concern
21
33
 
@@ -92,13 +104,13 @@ module Raix
92
104
  # TODO: add a standardized callback hook for usage events
93
105
  # broadcast(:usage_event, usage_subject, self.class.name.to_s, response, premium?)
94
106
 
95
- # TODO: handle parallel tool calls
96
- if (function = response.dig("choices", 0, "message", "tool_calls", 0, "function"))
97
- @current_function = function["name"]
98
- # dispatch the called function
99
- arguments = JSON.parse(function["arguments"].presence || "{}")
100
- arguments[:bot_message] = bot_message if respond_to?(:bot_message)
101
- return send(function["name"], arguments.with_indifferent_access)
107
+ tool_calls = response.dig("choices", 0, "message", "tool_calls") || []
108
+ if tool_calls.any?
109
+ return tool_calls.map do |tool_call|
110
+ # dispatch the called function
111
+ arguments = JSON.parse(tool_call["function"]["arguments"].presence || "{}")
112
+ send(tool_call["function"]["name"], arguments.with_indifferent_access)
113
+ end
102
114
  end
103
115
 
104
116
  response.tap do |res|
@@ -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.2"
4
+ VERSION = "0.4.4"
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.2
4
+ version: 0.4.4
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-05 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