raix 0.6 → 0.7.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: 4bb905086f308c10375b54337b9cb4effe9387ed7e7a6a0dd84875176984e29c
4
- data.tar.gz: dbc3b0d67aa91d8dc9b8ba9504b84d52617529c2a8e69fd0d21f3ad4e59df31d
3
+ metadata.gz: 9a2990e8225edcfbcc5007aae41a0dd69987bff18bd085bb9c486561e5597044
4
+ data.tar.gz: 60ef3efa8eb6cbe1b445afd09fc65a4b1d6fc62c872588956baee46b511d79d3
5
5
  SHA512:
6
- metadata.gz: cbbaebe7bd8d18d2cd82b41b7a4bcf468eda1483485b2a707ff980d51068dea1e9ef5e20510dc7dd46bf1982fc7db14054399919842a28930cb12e37fa016022
7
- data.tar.gz: 61037ede25456a15053e96dc1bae11125a9d3e0d5dc08e255087c9b69a2c4d096fc2e95d27f600a955bd93b421d8ed703b7c7b0df8de35812b82dbf9f0818858
6
+ metadata.gz: dfa0a7dfed2782d49c95f749c847b024ef41097b4ab318b34f9eabbfd9f754cf49c6eb859c1b14534332efea930d707df457e2d24a4c1f30f043d7dacb3c801c
7
+ data.tar.gz: 2e291a98b2ed5e25c2381a30d609e4277cb94458531c03e77b79fc95e70ee92e40c0d9cacb31809a1f7acca860550e4d02698856321869144dc56a4d794ea3d8
data/.rubocop.yml CHANGED
@@ -10,8 +10,10 @@ Style/StringLiteralsInInterpolation:
10
10
  Enabled: true
11
11
  EnforcedStyle: double_quotes
12
12
 
13
+ Style/IfUnlessModifier:
14
+ Enabled: false
13
15
  Layout/LineLength:
14
- Max: 180
16
+ Enabled: false
15
17
 
16
18
  Metrics/BlockLength:
17
19
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,4 +1,22 @@
1
- ## [Unreleased]
1
+ ## [0.7] - 2024-04-02
2
+ - adds support for `until` condition in `PromptDeclarations` to control prompt looping
3
+ - adds support for `if` and `unless` conditions in `PromptDeclarations` to control prompt execution
4
+ - adds support for `success` callback in `PromptDeclarations` to handle prompt responses
5
+ - adds support for `stream` handler in `PromptDeclarations` to control response streaming
6
+ - adds support for `params` in `PromptDeclarations` to customize API parameters per prompt
7
+ - adds support for `system` directive in `PromptDeclarations` to set per-prompt system messages
8
+ - adds support for `call` in `PromptDeclarations` to delegate to callable prompt objects
9
+ - adds support for `text` in `PromptDeclarations` to specify prompt content via lambda, string, or symbol
10
+ - adds support for `raw` parameter in `PromptDeclarations` to return raw API responses
11
+ - adds support for `openai` parameter in `PromptDeclarations` to use OpenAI directly
12
+ - adds support for `prompt` parameter in `PromptDeclarations` to specify initial prompt
13
+ - adds support for `last_response` in `PromptDeclarations` to access previous prompt responses
14
+ - adds support for `current_prompt` in `PromptDeclarations` to access current prompt context
15
+ - adds support for `MAX_LOOP_COUNT` in `PromptDeclarations` to prevent infinite loops
16
+ - adds support for `execute_ai_request` in `PromptDeclarations` to handle API calls
17
+ - adds support for `chat_completion_from_superclass` in `PromptDeclarations` to handle superclass calls
18
+ - adds support for `model`, `temperature`, and `max_tokens` in `PromptDeclarations` to access prompt parameters
19
+ - Make automatic JSON parsing available to non-OpenAI providers that don't support the response_format parameter by scanning for json XML tags
2
20
 
3
21
  ## [0.6.0] - 2024-11-12
4
22
  - adds `save_response` option to `chat_completion` to control transcript updates
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- raix (0.6)
4
+ raix (0.7.1)
5
5
  activesupport (>= 6.0)
6
6
  open_router (~> 0.2)
7
7
  ruby-openai (~> 7.0)
data/README.llm ADDED
@@ -0,0 +1,106 @@
1
+ # Raix (Ruby AI eXtensions)
2
+ Raix adds LLM-based AI functionality to Ruby classes. It supports OpenAI or OpenRouter as providers and can work in non-Rails apps if you include ActiveSupport.
3
+
4
+ ## Chat Completion
5
+ You must include `Raix::ChatCompletion`. It gives you a `transcript` array for messages and a `chat_completion` method that sends them to the AI.
6
+
7
+ ```ruby
8
+ class MeaningOfLife
9
+ include Raix::ChatCompletion
10
+ end
11
+
12
+ ai = MeaningOfLife.new
13
+ ai.transcript << { user: "What is the meaning of life?" }
14
+ puts ai.chat_completion
15
+ ```
16
+
17
+ You can add messages using either `{ user: "..." }` or `{ role: "user", content: "..." }`.
18
+
19
+ ### Predicted Outputs
20
+ Pass `prediction` to support [Predicted Outputs](https://platform.openai.com/docs/guides/latency-optimization#use-predicted-outputs):
21
+ ```ruby
22
+ ai.chat_completion(openai: "gpt-4o", params: { prediction: "..." })
23
+ ```
24
+
25
+ ### Prompt Caching
26
+ When using Anthropic models, you can specify `cache_at`. Messages above that size get sent as ephemeral multipart segments.
27
+ ```ruby
28
+ ai.chat_completion(params: { cache_at: 1000 })
29
+ ```
30
+
31
+ ## Function Dispatch
32
+ Include `Raix::FunctionDispatch` to declare functions AI can call in a chat loop. Use `chat_completion(loop: true)` so the AI can call functions and generate more messages until it outputs a final text response.
33
+
34
+ ```ruby
35
+ class WhatIsTheWeather
36
+ include Raix::ChatCompletion
37
+ include Raix::FunctionDispatch
38
+
39
+ function :check_weather, "Check the weather for a location", location: { type: "string" } do |args|
40
+ "The weather in #{args[:location]} is hot and sunny"
41
+ end
42
+ end
43
+ ```
44
+
45
+ If the AI calls multiple functions at once, Raix handles them in sequence and returns an array of results. Call `stop_looping!` inside a function to end the loop.
46
+
47
+ ## Prompt Declarations
48
+ Include `Raix::PromptDeclarations` to define a chain of prompts in order. Each prompt can be inline text or a callable class that also includes `ChatCompletion`.
49
+
50
+ ```ruby
51
+ class PromptSubscriber
52
+ include Raix::ChatCompletion
53
+ include Raix::PromptDeclarations
54
+
55
+ prompt call: FetchUrlCheck
56
+ prompt call: MemoryScan
57
+ prompt text: -> { user_message.content }
58
+
59
+ def message_created(user_message)
60
+ chat_completion(loop: true, openai: "gpt-4o")
61
+ end
62
+ end
63
+ ```
64
+
65
+ ## Predicate Module
66
+ Include `Raix::Predicate` to handle yes/no/maybe questions. Define blocks with the `yes?`, `no?`, and `maybe?` methods.
67
+
68
+ ```ruby
69
+ class Question
70
+ include Raix::Predicate
71
+
72
+ yes? { |explanation| puts "Affirmative: #{explanation}" }
73
+ no? { |explanation| puts "Negative: #{explanation}" }
74
+
75
+ end
76
+ ```
77
+
78
+ ## ResponseFormat (Experimental)
79
+ Use `Raix::ResponseFormat` to enforce JSON schemas for structured responses.
80
+
81
+ ```ruby
82
+ format = Raix::ResponseFormat.new("PersonInfo", {
83
+ name: { type: "string" },
84
+ age: { type: "integer" }
85
+ })
86
+
87
+ class StructuredResponse
88
+ include Raix::ChatCompletion
89
+
90
+ def analyze_person(name)
91
+ chat_completion(response_format: format)
92
+ end
93
+ end
94
+ ```
95
+
96
+ ## Installation
97
+ Add `gem "raix"` to your Gemfile or run `gem install raix`. Configure an OpenRouter or OpenAI client in an initializer:
98
+
99
+ ```ruby
100
+ # config/initializers/raix.rb
101
+ Raix.configure do |config|
102
+ config.openrouter_client = OpenRouter::Client.new
103
+ end
104
+ ```
105
+ Make sure you have valid API tokens for your chosen provider.
106
+ ```
data/README.md CHANGED
@@ -24,6 +24,10 @@ end
24
24
  => "The question of the meaning of life is one of the most profound and enduring inquiries in philosophy, religion, and science.
25
25
  Different perspectives offer various answers..."
26
26
 
27
+ By default, Raix will automatically add the AI's response to the transcript. This behavior can be controlled with the `save_response` parameter, which defaults to `true`. You may want to set it to `false` when making multiple chat completion calls during the lifecycle of a single object (whether sequentially or in parallel) and want to manage the transcript updates yourself:
28
+
29
+ ```ruby
30
+ >> ai.chat_completion(save_response: false)
27
31
  ```
28
32
 
29
33
  #### Transcript Format
@@ -74,6 +78,26 @@ Note that there is a limit of four breakpoints, and the cache will expire within
74
78
  },
75
79
  ```
76
80
 
81
+ ### JSON Mode
82
+
83
+ Raix supports JSON mode for chat completions, which ensures that the AI model's response is valid JSON. This is particularly useful when you need structured data from the model.
84
+
85
+ When using JSON mode with OpenAI models, Raix will automatically set the `response_format` parameter on requests accordingly, and attempt to parse the entire response body as JSON.
86
+ When using JSON mode with other models (e.g. Anthropic) that don't support `response_format`, Raix will look for JSON content inside of &lt;json&gt; XML tags in the response, before
87
+ falling back to parsing the entire response body. Make sure you tell the AI to reply with JSON inside of XML tags.
88
+
89
+ ```ruby
90
+ >> my_class.chat_completion(json: true)
91
+ => { "key": "value" }
92
+ ```
93
+
94
+ When using JSON mode with non-OpenAI providers, Raix automatically sets the `require_parameters` flag to ensure proper JSON formatting. You can also combine JSON mode with other parameters:
95
+
96
+ ```ruby
97
+ >> my_class.chat_completion(json: true, openai: "gpt-4o")
98
+ => { "key": "value" }
99
+ ```
100
+
77
101
  ### Use of Tools/Functions
78
102
 
79
103
  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.
@@ -105,26 +129,39 @@ Note that for security reasons, dispatching functions only works with functions
105
129
 
106
130
  #### Multiple Tool Calls
107
131
 
108
- Some AI models (like GPT-4) can make multiple tool calls in a single response. When this happens, Raix will automatically handle all the function calls sequentially and return an array of their results. Here's an example:
132
+ Some AI models (like GPT-4) can make multiple tool calls in a single response. When this happens, Raix will automatically handle all the function calls sequentially.
133
+ If you need to capture the arguments to the function calls, do so in the block passed to `function`. The response from `chat_completion` is always the final text
134
+ response from the assistant, and is not affected by function calls.
109
135
 
110
136
  ```ruby
111
137
  class MultipleToolExample
112
138
  include Raix::ChatCompletion
113
139
  include Raix::FunctionDispatch
114
140
 
141
+ attr_reader :invocations
142
+
115
143
  function :first_tool do |arguments|
144
+ @invocations << :first
116
145
  "Result from first tool"
117
146
  end
118
147
 
119
148
  function :second_tool do |arguments|
149
+ @invocations << :second
120
150
  "Result from second tool"
121
151
  end
152
+
153
+ def initialize
154
+ @invocations = []
155
+ end
122
156
  end
123
157
 
124
158
  example = MultipleToolExample.new
125
159
  example.transcript << { user: "Please use both tools" }
126
- results = example.chat_completion(openai: "gpt-4o")
127
- # => ["Result from first tool", "Result from second tool"]
160
+ example.chat_completion(openai: "gpt-4o")
161
+ # => "I used both tools, as requested"
162
+
163
+ example.invocations
164
+ # => [:first, :second]
128
165
  ```
129
166
 
130
167
  #### Manually Stopping a Loop
@@ -222,7 +259,6 @@ class PromptSubscriber
222
259
  end
223
260
 
224
261
  ...
225
-
226
262
  end
227
263
 
228
264
  class FetchUrlCheck
@@ -264,6 +300,80 @@ Notably, Olympia does not use the `FunctionDispatch` module in its primary conve
264
300
 
265
301
  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.
266
302
 
303
+ #### Additional PromptDeclarations Options
304
+
305
+ The `PromptDeclarations` module supports several additional options that can be used to customize prompt behavior:
306
+
307
+ ```ruby
308
+ class CustomPromptExample
309
+ include Raix::ChatCompletion
310
+ include Raix::PromptDeclarations
311
+
312
+ # Basic prompt with text
313
+ prompt text: "Process this input"
314
+
315
+ # Prompt with system directive
316
+ prompt system: "You are a helpful assistant",
317
+ text: "Analyze this text"
318
+
319
+ # Prompt with conditions
320
+ prompt text: "Process this input",
321
+ if: -> { some_condition },
322
+ unless: -> { some_other_condition }
323
+
324
+ # Prompt with success callback
325
+ prompt text: "Process this input",
326
+ success: ->(response) { handle_response(response) }
327
+
328
+ # Prompt with custom parameters
329
+ prompt text: "Process with custom settings",
330
+ params: { temperature: 0.7, max_tokens: 1000 }
331
+
332
+ # Prompt with until condition for looping
333
+ prompt text: "Keep processing until complete",
334
+ until: -> { processing_complete? }
335
+
336
+ # Prompt with raw response
337
+ prompt text: "Get raw response",
338
+ raw: true
339
+
340
+ # Prompt using OpenAI directly
341
+ prompt text: "Use OpenAI",
342
+ openai: true
343
+ end
344
+ ```
345
+
346
+ The available options include:
347
+
348
+ - `system`: Set a system directive for the prompt
349
+ - `if`/`unless`: Control prompt execution with conditions
350
+ - `success`: Handle prompt responses with callbacks
351
+ - `params`: Customize API parameters per prompt
352
+ - `until`: Control prompt looping
353
+ - `raw`: Get raw API responses
354
+ - `openai`: Use OpenAI directly
355
+ - `stream`: Control response streaming
356
+ - `call`: Delegate to callable prompt objects
357
+
358
+ You can also access the current prompt context and previous responses:
359
+
360
+ ```ruby
361
+ class ContextAwarePrompt
362
+ include Raix::ChatCompletion
363
+ include Raix::PromptDeclarations
364
+
365
+ def process_with_context
366
+ # Access current prompt
367
+ current_prompt.params[:temperature]
368
+
369
+ # Access previous response
370
+ last_response
371
+
372
+ chat_completion
373
+ end
374
+ end
375
+ ```
376
+
267
377
  ## Predicate Module
268
378
 
269
379
  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).
@@ -418,7 +528,7 @@ class StructuredResponse
418
528
  })
419
529
 
420
530
  transcript << { user: "Analyze the person named #{name}" }
421
- chat_completion(response_format: format)
531
+ chat_completion(params: { response_format: format })
422
532
  end
423
533
  end
424
534
 
@@ -40,7 +40,7 @@ module Raix
40
40
  #
41
41
  # @param params [Hash] The parameters for chat completion.
42
42
  # @option loop [Boolean] :loop (false) Whether to loop the chat completion after function calls.
43
- # @option params [Boolean] :json (false) Whether to return the parse the response as a JSON object.
43
+ # @option params [Boolean] :json (false) Whether to return the parse the response as a JSON object. Will search for <json> tags in the response first, then fall back to the default JSON parsing of the entire response.
44
44
  # @option params [Boolean] :openai (false) Whether to use OpenAI's API instead of OpenRouter's.
45
45
  # @option params [Boolean] :raw (false) Whether to return the raw response or dig the text content.
46
46
  # @return [String|Hash] The completed chat response.
@@ -124,9 +124,12 @@ module Raix
124
124
  content = res.dig("choices", 0, "message", "content")
125
125
 
126
126
  transcript << { assistant: content } if save_response
127
+ content = content.squish
127
128
 
128
129
  if json
129
- content = content.squish
130
+ # Make automatic JSON parsing available to non-OpenAI providers that don't support the response_format parameter
131
+ content = content.match(%r{<json>(.*?)</json>}m)[1] if content.include?("<json>")
132
+
130
133
  return JSON.parse(content)
131
134
  end
132
135
 
@@ -1,119 +1,176 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ostruct"
4
- require "active_support/core_ext/string/filters"
5
-
6
- module Raix
7
- # The PromptDeclarations module provides a way to chain prompts and handle
8
- # user responses in a serialized manner (in the order they were defined),
9
- # with support for functions if the FunctionDispatch module is also included.
10
- module PromptDeclarations
11
- extend ActiveSupport::Concern
12
- extend ChatCompletion
13
-
14
- module ClassMethods # rubocop:disable Style/Documentation
15
- # Adds a prompt to the list of prompts.
16
- #
17
- # @param system [Proc] A lambda that generates the system message.
18
- # @param text [Proc] A lambda that generates the prompt text. (Required)
19
- # @param success [Proc] The block of code to execute when the prompt is answered.
20
- # @param parameters [Hash] Additional parameters for the completion API call
21
- # @param stream [Boolean] Whether to stream the response.
22
- def prompt(text:, system: nil, success: nil, params: {}, stream: false)
23
- name = Digest::SHA256.hexdigest(text.inspect)[0..7]
24
- prompts << begin
25
- OpenStruct.new({ name:, system:, text:, success:, params:, stream: })
26
- end
27
-
28
- define_method(name) do |response|
29
- if Rails.env.local?
30
- puts "_" * 80
31
- puts "PromptDeclarations#response:"
32
- puts "#{text.source_location} (#{name})"
33
- puts response
34
- puts "_" * 80
35
- end
36
-
37
- return response if success.nil?
38
- return send(success, response) if success.is_a?(Symbol)
39
-
40
- instance_exec(response, &success)
41
- end
42
- end
43
4
 
44
- # the list of prompts declared at class level
45
- def prompts
46
- @prompts ||= []
47
- end
5
+ # This module provides a way to chain prompts and handle
6
+ # user responses in a serialized manner, with support for
7
+ # functions if the FunctionDispatch module is also included.
8
+ module PromptDeclarations
9
+ extend ActiveSupport::Concern
48
10
 
49
- # getter/setter for system prompt declared at class level
50
- def system_prompt(prompt = nil)
51
- prompt ? @system_prompt = prompt.squish : @system_prompt
11
+ module ClassMethods # rubocop:disable Style/Documentation
12
+ # Adds a prompt to the list of prompts. At minimum, provide a `text` or `call` parameter.
13
+ #
14
+ # @param system [Proc] A lambda that generates the system message.
15
+ # @param call [ChatCompletion] A callable class that includes ChatCompletion. Will be passed a context object when initialized.
16
+ # @param text Accepts 1) a lambda that returns the prompt text, 2) a string, or 3) a symbol that references a method.
17
+ # @param stream [Proc] A lambda stream handler
18
+ # @param success [Proc] The block of code to execute when the prompt is answered.
19
+ # @param params [Hash] Additional parameters for the completion API call
20
+ # @param if [Proc] A lambda that determines if the prompt should be executed.
21
+ def prompt(system: nil, call: nil, text: nil, stream: nil, success: nil, params: {}, if: nil, unless: nil, until: nil)
22
+ name = Digest::SHA256.hexdigest(text.inspect)[0..7]
23
+ prompts << OpenStruct.new({ name:, system:, call:, text:, stream:, success:, if:, unless:, until:, params: })
24
+
25
+ define_method(name) do |response|
26
+ puts "_" * 80
27
+ puts "PromptDeclarations#response:"
28
+ puts "#{text&.source_location} (#{name})"
29
+ puts response
30
+ puts "_" * 80
31
+
32
+ return response if success.nil?
33
+ return send(success, response) if success.is_a?(Symbol)
34
+
35
+ instance_exec(response, &success)
52
36
  end
53
37
  end
54
38
 
55
- # Executes the chat completion process based on the class-level declared prompts.
56
- # The response to each prompt is added to the transcript automatically and returned.
57
- #
58
- # Prompts require at least a `text` lambda parameter.
59
- #
60
- # @param params [Hash] Parameters for the chat completion override those defined in the current prompt.
61
- # @option params [Boolean] :raw (false) Whether to return the raw response or dig the text content.
62
- #
63
- # Uses system prompt in following order of priority:
64
- # - system lambda specified in the prompt declaration
65
- # - system_prompt instance method if defined
66
- # - system_prompt class-level declaration if defined
67
- #
68
- # TODO: shortcut syntax passes just a string prompt if no other options are needed.
69
- #
70
- # @raise [RuntimeError] If no prompts are defined.
71
- #
72
- def chat_completion(params: {}, raw: false)
73
- raise "No prompts defined" unless self.class.prompts.present?
74
-
75
- current_prompts = self.class.prompts.clone
39
+ def prompts
40
+ @prompts ||= []
41
+ end
42
+ end
76
43
 
77
- while (@current_prompt = current_prompts.shift)
78
- __system_prompt = instance_exec(&@current_prompt.system) if @current_prompt.system.present? # rubocop:disable Lint/UnderscorePrefixedVariableName
44
+ attr_reader :current_prompt, :last_response
45
+
46
+ MAX_LOOP_COUNT = 5
47
+
48
+ # Executes the chat completion process based on the class-level declared prompts.
49
+ # The response to each prompt is added to the transcript automatically and returned.
50
+ #
51
+ # Raises an error if there are not enough prompts defined.
52
+ #
53
+ # Uses system prompt in following order of priority:
54
+ # - system lambda specified in the prompt declaration
55
+ # - system_prompt instance method if defined
56
+ # - system_prompt class-level declaration if defined
57
+ #
58
+ # Prompts require a text lambda to be defined at minimum.
59
+ # TODO: shortcut syntax passes just a string prompt if no other options are needed.
60
+ #
61
+ # @raise [RuntimeError] If no prompts are defined.
62
+ #
63
+ # @param prompt [String] The prompt to use for the chat completion.
64
+ # @param params [Hash] Parameters for the chat completion.
65
+ # @param raw [Boolean] Whether to return the raw response.
66
+ #
67
+ # TODO: SHOULD NOT HAVE A DIFFERENT INTERFACE THAN PARENT
68
+ def chat_completion(prompt = nil, params: {}, raw: false, openai: false)
69
+ raise "No prompts defined" unless self.class.prompts.present?
70
+
71
+ loop_count = 0
72
+
73
+ current_prompts = self.class.prompts.clone
74
+
75
+ while (@current_prompt = current_prompts.shift)
76
+ next if @current_prompt.if.present? && !instance_exec(&@current_prompt.if)
77
+ next if @current_prompt.unless.present? && instance_exec(&@current_prompt.unless)
78
+
79
+ input = case current_prompt.text
80
+ when Proc
81
+ instance_exec(&current_prompt.text)
82
+ when String
83
+ current_prompt.text
84
+ when Symbol
85
+ send(current_prompt.text)
86
+ else
87
+ last_response.presence || prompt
88
+ end
89
+
90
+ if current_prompt.call.present?
91
+ Rails.logger.debug "Calling #{current_prompt.call} with input: #{input}"
92
+ current_prompt.call.new(self).call(input).tap do |response|
93
+ if response.present?
94
+ transcript << { assistant: response }
95
+ @last_response = send(current_prompt.name, response)
96
+ end
97
+ end
98
+ else
99
+ __system_prompt = instance_exec(&current_prompt.system) if current_prompt.system.present? # rubocop:disable Lint/UnderscorePrefixedVariableName
79
100
  __system_prompt ||= system_prompt if respond_to?(:system_prompt)
80
101
  __system_prompt ||= self.class.system_prompt.presence
81
102
  transcript << { system: __system_prompt } if __system_prompt
82
- transcript << { user: instance_exec(&@current_prompt.text) } # text is required
103
+ transcript << { user: instance_exec(&current_prompt.text) } # text is required
83
104
 
84
- params = @current_prompt.params.merge(params)
105
+ params = current_prompt.params.merge(params)
85
106
 
86
107
  # set the stream if necessary
87
- self.stream = instance_exec(&@current_prompt.stream) if @current_prompt.stream.present?
108
+ self.stream = instance_exec(&current_prompt.stream) if current_prompt.stream.present?
88
109
 
89
- super(params:, raw:).then do |response|
90
- transcript << { assistant: response }
91
- @last_response = send(@current_prompt.name, response)
92
- end
110
+ execute_ai_request(params:, raw:, openai:, transcript:, loop_count:)
93
111
  end
94
112
 
95
- @last_response
113
+ next unless current_prompt.until.present? && !instance_exec(&current_prompt.until)
114
+
115
+ if loop_count >= MAX_LOOP_COUNT
116
+ Honeybadger.notify(
117
+ "Max loop count reached in chat_completion. Forcing return.",
118
+ context: {
119
+ current_prompts:,
120
+ prompt:,
121
+ usage_subject: usage_subject.inspect,
122
+ last_response: Current.or_response
123
+ }
124
+ )
125
+
126
+ return last_response
127
+ else
128
+ current_prompts.unshift(@current_prompt) # put it back at the front
129
+ loop_count += 1
130
+ end
96
131
  end
97
132
 
98
- # Returns the model parameter of the current prompt or the default model.
99
- #
100
- # @return [Object] The model parameter of the current prompt or the default model.
101
- def model
102
- @current_prompt.params[:model] || super
103
- end
133
+ last_response
134
+ end
104
135
 
105
- # Returns the temperature parameter of the current prompt or the default temperature.
106
- #
107
- # @return [Float] The temperature parameter of the current prompt or the default temperature.
108
- def temperature
109
- @current_prompt.params[:temperature] || super
136
+ def execute_ai_request(params:, raw:, openai:, transcript:, loop_count:)
137
+ chat_completion_from_superclass(params:, raw:, openai:).then do |response|
138
+ transcript << { assistant: response }
139
+ @last_response = send(current_prompt.name, response)
140
+ self.stream = nil # clear it again so it's not used for the next prompt
110
141
  end
142
+ rescue Conversation::StreamError => e
143
+ # Bubbles the error up the stack if no loops remain
144
+ raise Faraday::ServerError.new(nil, { status: e.status, body: e.response }) if loop_count >= MAX_LOOP_COUNT
111
145
 
112
- # Returns the max_tokens parameter of the current prompt or the default max_tokens.
113
- #
114
- # @return [Integer] The max_tokens parameter of the current prompt or the default max_tokens.
115
- def max_tokens
116
- @current_prompt.params[:max_tokens] || super
117
- end
146
+ sleep 1.second # Wait before continuing
147
+ end
148
+
149
+ # Returns the model parameter of the current prompt or the default model.
150
+ #
151
+ # @return [Object] The model parameter of the current prompt or the default model.
152
+ def model
153
+ @current_prompt.params[:model] || super
154
+ end
155
+
156
+ # Returns the temperature parameter of the current prompt or the default temperature.
157
+ #
158
+ # @return [Float] The temperature parameter of the current prompt or the default temperature.
159
+ def temperature
160
+ @current_prompt.params[:temperature] || super
161
+ end
162
+
163
+ # Returns the max_tokens parameter of the current prompt or the default max_tokens.
164
+ #
165
+ # @return [Integer] The max_tokens parameter of the current prompt or the default max_tokens.
166
+ def max_tokens
167
+ @current_prompt.params[:max_tokens] || super
168
+ end
169
+
170
+ protected
171
+
172
+ # workaround for super.chat_completion, which is not available in ruby
173
+ def chat_completion_from_superclass(*args, **kargs)
174
+ method(:chat_completion).super_method.call(*args, **kargs)
118
175
  end
119
176
  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.6"
4
+ VERSION = "0.7.1"
5
5
  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.6'
4
+ version: 0.7.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: 2025-04-02 00:00:00.000000000 Z
11
+ date: 2025-04-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -68,6 +68,7 @@ files:
68
68
  - Gemfile.lock
69
69
  - Guardfile
70
70
  - LICENSE.txt
71
+ - README.llm
71
72
  - README.md
72
73
  - Rakefile
73
74
  - lib/raix.rb