ruby_llm-agents 0.3.6 → 0.4.0
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/lib/ruby_llm/agents/base/dsl.rb +65 -13
- data/lib/ruby_llm/agents/base/execution.rb +30 -6
- data/lib/ruby_llm/agents/base/reliability_dsl.rb +82 -0
- data/lib/ruby_llm/agents/base.rb +27 -0
- data/lib/ruby_llm/agents/deprecations.rb +77 -0
- data/lib/ruby_llm/agents/reliability/breaker_manager.rb +80 -0
- data/lib/ruby_llm/agents/reliability/execution_constraints.rb +69 -0
- data/lib/ruby_llm/agents/reliability/executor.rb +124 -0
- data/lib/ruby_llm/agents/reliability/fallback_routing.rb +72 -0
- data/lib/ruby_llm/agents/reliability/retry_strategy.rb +76 -0
- data/lib/ruby_llm/agents/result.rb +72 -2
- data/lib/ruby_llm/agents/version.rb +1 -1
- data/lib/ruby_llm/agents.rb +6 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d8bfa346ee4010060508948c994bc719f5f1ff40dd2eec3aeb04500f2054341
|
|
4
|
+
data.tar.gz: 5546ca87104e12ba0522c2a6161ed2917355fb6fffcc7cc6dffd9920963b1037
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 391c1202b7bb677337329b5bd5358e9cb5bc4589d37cf48291bb4ff427d8ba1e3e7db99e8787d079ecd2f08382a4aa9940c573ad33c3be2040aa2e6ec21eb353
|
|
7
|
+
data.tar.gz: 9b1ae37b9ca74f060377d32232a9c26a33e411d285ac658be9508348d75316582aa47e309b85c3258fcaf33a1d5e103fdef2aa1090569b86140b046ee7e975fc
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "reliability_dsl"
|
|
4
|
+
|
|
3
5
|
module RubyLLM
|
|
4
6
|
module Agents
|
|
5
7
|
class Base
|
|
@@ -74,6 +76,31 @@ module RubyLLM
|
|
|
74
76
|
|
|
75
77
|
# @!group Reliability DSL
|
|
76
78
|
|
|
79
|
+
# Configures reliability features using a block syntax
|
|
80
|
+
#
|
|
81
|
+
# Groups all reliability configuration in a single block for clarity.
|
|
82
|
+
# Individual methods (retries, fallback_models, etc.) remain available
|
|
83
|
+
# for backward compatibility.
|
|
84
|
+
#
|
|
85
|
+
# @yield Block containing reliability configuration
|
|
86
|
+
# @return [void]
|
|
87
|
+
# @example
|
|
88
|
+
# reliability do
|
|
89
|
+
# retries max: 3, backoff: :exponential
|
|
90
|
+
# fallback_models "gpt-4o-mini"
|
|
91
|
+
# total_timeout 30
|
|
92
|
+
# circuit_breaker errors: 5
|
|
93
|
+
# end
|
|
94
|
+
def reliability(&block)
|
|
95
|
+
builder = ReliabilityDSL.new
|
|
96
|
+
builder.instance_eval(&block)
|
|
97
|
+
|
|
98
|
+
@retries_config = builder.retries_config if builder.retries_config
|
|
99
|
+
@fallback_models = builder.fallback_models_list if builder.fallback_models_list.any?
|
|
100
|
+
@total_timeout = builder.total_timeout_value if builder.total_timeout_value
|
|
101
|
+
@circuit_breaker_config = builder.circuit_breaker_config if builder.circuit_breaker_config
|
|
102
|
+
end
|
|
103
|
+
|
|
77
104
|
# Configures retry behavior for this agent
|
|
78
105
|
#
|
|
79
106
|
# @param max [Integer] Maximum number of retry attempts (default: 0)
|
|
@@ -162,13 +189,18 @@ module RubyLLM
|
|
|
162
189
|
# @param name [Symbol] The parameter name
|
|
163
190
|
# @param required [Boolean] Whether the parameter is required
|
|
164
191
|
# @param default [Object, nil] Default value if not provided
|
|
192
|
+
# @param type [Class, nil] Optional type for validation (e.g., String, Integer, Array)
|
|
165
193
|
# @return [void]
|
|
166
|
-
# @example
|
|
194
|
+
# @example Without type (accepts anything)
|
|
167
195
|
# param :query, required: true
|
|
168
|
-
# param :
|
|
169
|
-
|
|
196
|
+
# param :data, default: {}
|
|
197
|
+
# @example With type validation
|
|
198
|
+
# param :limit, default: 10, type: Integer
|
|
199
|
+
# param :name, type: String
|
|
200
|
+
# param :tags, type: Array
|
|
201
|
+
def param(name, required: false, default: nil, type: nil)
|
|
170
202
|
@params ||= {}
|
|
171
|
-
@params[name] = { required: required, default: default }
|
|
203
|
+
@params[name] = { required: required, default: default, type: type }
|
|
172
204
|
define_method(name) do
|
|
173
205
|
@options[name] || @options[name.to_s] || self.class.params.dig(name, :default)
|
|
174
206
|
end
|
|
@@ -186,17 +218,37 @@ module RubyLLM
|
|
|
186
218
|
|
|
187
219
|
# @!group Caching DSL
|
|
188
220
|
|
|
189
|
-
# Enables caching for this agent with
|
|
221
|
+
# Enables caching for this agent with explicit TTL
|
|
222
|
+
#
|
|
223
|
+
# This is the preferred method for enabling caching.
|
|
190
224
|
#
|
|
191
225
|
# @param ttl [ActiveSupport::Duration] Time-to-live for cached responses
|
|
192
226
|
# @return [void]
|
|
193
227
|
# @example
|
|
194
|
-
#
|
|
195
|
-
|
|
228
|
+
# cache_for 1.hour
|
|
229
|
+
# cache_for 30.minutes
|
|
230
|
+
def cache_for(ttl)
|
|
196
231
|
@cache_enabled = true
|
|
197
232
|
@cache_ttl = ttl
|
|
198
233
|
end
|
|
199
234
|
|
|
235
|
+
# Enables caching for this agent with optional TTL
|
|
236
|
+
#
|
|
237
|
+
# @deprecated Use {#cache_for} instead for clarity.
|
|
238
|
+
# This method will be removed in version 1.0.
|
|
239
|
+
# @param ttl [ActiveSupport::Duration] Time-to-live for cached responses
|
|
240
|
+
# @return [void]
|
|
241
|
+
# @example
|
|
242
|
+
# cache 1.hour # deprecated
|
|
243
|
+
# cache_for 1.hour # preferred
|
|
244
|
+
def cache(ttl = CACHE_TTL)
|
|
245
|
+
RubyLLM::Agents::Deprecations.warn(
|
|
246
|
+
"cache(ttl) is deprecated. Use cache_for(ttl) instead for clarity.",
|
|
247
|
+
caller
|
|
248
|
+
)
|
|
249
|
+
cache_for(ttl)
|
|
250
|
+
end
|
|
251
|
+
|
|
200
252
|
# Returns whether caching is enabled for this agent
|
|
201
253
|
#
|
|
202
254
|
# @return [Boolean] true if caching is enabled
|
|
@@ -243,13 +295,13 @@ module RubyLLM
|
|
|
243
295
|
#
|
|
244
296
|
# @param tool_classes [Array<Class>] Tool classes to make available
|
|
245
297
|
# @return [Array<Class>] The current tools
|
|
298
|
+
# @example With array (preferred)
|
|
299
|
+
# tools [WeatherTool, SearchTool, CalculatorTool]
|
|
246
300
|
# @example Single tool
|
|
247
|
-
# tools WeatherTool
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if tool_classes.any?
|
|
252
|
-
@tools = tool_classes.flatten
|
|
301
|
+
# tools [WeatherTool]
|
|
302
|
+
def tools(tool_classes = nil)
|
|
303
|
+
if tool_classes
|
|
304
|
+
@tools = Array(tool_classes)
|
|
253
305
|
end
|
|
254
306
|
@tools || inherited_or_default(:tools, RubyLLM::Agents.configuration.default_tools)
|
|
255
307
|
end
|
|
@@ -62,7 +62,7 @@ module RubyLLM
|
|
|
62
62
|
reset_accumulated_tool_calls!
|
|
63
63
|
|
|
64
64
|
Timeout.timeout(self.class.timeout) do
|
|
65
|
-
if
|
|
65
|
+
if streaming_enabled? && block_given?
|
|
66
66
|
execute_with_streaming(current_client, &block)
|
|
67
67
|
else
|
|
68
68
|
response = current_client.ask(user_prompt, **ask_options)
|
|
@@ -177,6 +177,16 @@ module RubyLLM
|
|
|
177
177
|
config[:circuit_breaker].present?
|
|
178
178
|
end
|
|
179
179
|
|
|
180
|
+
# Returns whether streaming is enabled for this execution
|
|
181
|
+
#
|
|
182
|
+
# Checks both class-level DSL setting and instance-level override
|
|
183
|
+
# (set by the stream class method).
|
|
184
|
+
#
|
|
185
|
+
# @return [Boolean] true if streaming is enabled
|
|
186
|
+
def streaming_enabled?
|
|
187
|
+
@force_streaming || self.class.streaming
|
|
188
|
+
end
|
|
189
|
+
|
|
180
190
|
# Returns options to pass to the ask method
|
|
181
191
|
#
|
|
182
192
|
# Currently supports :with for attachments (images, PDFs, etc.)
|
|
@@ -188,14 +198,28 @@ module RubyLLM
|
|
|
188
198
|
opts
|
|
189
199
|
end
|
|
190
200
|
|
|
191
|
-
# Validates that all required parameters are present
|
|
201
|
+
# Validates that all required parameters are present and types match
|
|
192
202
|
#
|
|
193
|
-
# @raise [ArgumentError] If required parameters are missing
|
|
203
|
+
# @raise [ArgumentError] If required parameters are missing or types don't match
|
|
194
204
|
# @return [void]
|
|
195
205
|
def validate_required_params!
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
206
|
+
self.class.params.each do |name, config|
|
|
207
|
+
value = @options[name] || @options[name.to_s]
|
|
208
|
+
has_value = @options.key?(name) || @options.key?(name.to_s)
|
|
209
|
+
|
|
210
|
+
# Check required
|
|
211
|
+
if config[:required] && !has_value
|
|
212
|
+
raise ArgumentError, "#{self.class} missing required param: #{name}"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Check type if specified and value is present (not nil)
|
|
216
|
+
if config[:type] && has_value && !value.nil?
|
|
217
|
+
unless value.is_a?(config[:type])
|
|
218
|
+
raise ArgumentError,
|
|
219
|
+
"#{self.class} expected #{config[:type]} for :#{name}, got #{value.class}"
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
199
223
|
end
|
|
200
224
|
|
|
201
225
|
# Builds and configures the RubyLLM client
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Base
|
|
6
|
+
# DSL builder for reliability configuration
|
|
7
|
+
#
|
|
8
|
+
# Provides a block-based configuration syntax for grouping
|
|
9
|
+
# all reliability settings together.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# class MyAgent < ApplicationAgent
|
|
13
|
+
# reliability do
|
|
14
|
+
# retries max: 3, backoff: :exponential
|
|
15
|
+
# fallback_models "gpt-4o-mini"
|
|
16
|
+
# total_timeout 30
|
|
17
|
+
# circuit_breaker errors: 5, within: 60
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @api public
|
|
22
|
+
class ReliabilityDSL
|
|
23
|
+
attr_reader :retries_config, :fallback_models_list, :total_timeout_value, :circuit_breaker_config
|
|
24
|
+
|
|
25
|
+
def initialize
|
|
26
|
+
@retries_config = nil
|
|
27
|
+
@fallback_models_list = []
|
|
28
|
+
@total_timeout_value = nil
|
|
29
|
+
@circuit_breaker_config = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Configures retry behavior
|
|
33
|
+
#
|
|
34
|
+
# @param max [Integer] Maximum retry attempts
|
|
35
|
+
# @param backoff [Symbol] :constant or :exponential
|
|
36
|
+
# @param base [Float] Base delay in seconds
|
|
37
|
+
# @param max_delay [Float] Maximum delay between retries
|
|
38
|
+
# @param on [Array<Class>] Additional error classes to retry on
|
|
39
|
+
# @return [void]
|
|
40
|
+
def retries(max: 0, backoff: :exponential, base: 0.4, max_delay: 3.0, on: [])
|
|
41
|
+
@retries_config = {
|
|
42
|
+
max: max,
|
|
43
|
+
backoff: backoff,
|
|
44
|
+
base: base,
|
|
45
|
+
max_delay: max_delay,
|
|
46
|
+
on: on
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Sets fallback models
|
|
51
|
+
#
|
|
52
|
+
# @param models [Array<String>] Model identifiers
|
|
53
|
+
# @return [void]
|
|
54
|
+
def fallback_models(*models)
|
|
55
|
+
@fallback_models_list = models.flatten
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Sets total timeout across all retry/fallback attempts
|
|
59
|
+
#
|
|
60
|
+
# @param seconds [Integer] Total timeout in seconds
|
|
61
|
+
# @return [void]
|
|
62
|
+
def total_timeout(seconds)
|
|
63
|
+
@total_timeout_value = seconds
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Configures circuit breaker
|
|
67
|
+
#
|
|
68
|
+
# @param errors [Integer] Failure threshold
|
|
69
|
+
# @param within [Integer] Rolling window in seconds
|
|
70
|
+
# @param cooldown [Integer] Cooldown period in seconds
|
|
71
|
+
# @return [void]
|
|
72
|
+
def circuit_breaker(errors: 10, within: 60, cooldown: 300)
|
|
73
|
+
@circuit_breaker_config = {
|
|
74
|
+
errors: errors,
|
|
75
|
+
within: within,
|
|
76
|
+
cooldown: cooldown
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/ruby_llm/agents/base.rb
CHANGED
|
@@ -83,6 +83,33 @@ module RubyLLM
|
|
|
83
83
|
def call(*args, **kwargs, &block)
|
|
84
84
|
new(*args, **kwargs).call(&block)
|
|
85
85
|
end
|
|
86
|
+
|
|
87
|
+
# Streams agent execution, yielding chunks as they arrive
|
|
88
|
+
#
|
|
89
|
+
# A more explicit alternative to passing a block to call.
|
|
90
|
+
# Forces streaming mode for this invocation regardless of class setting.
|
|
91
|
+
#
|
|
92
|
+
# @param kwargs [Hash] Agent parameters
|
|
93
|
+
# @yield [chunk] Yields each chunk as it arrives
|
|
94
|
+
# @yieldparam chunk [RubyLLM::Chunk] Streaming chunk with content
|
|
95
|
+
# @return [Result] The final result after streaming completes
|
|
96
|
+
# @raise [ArgumentError] If no block is provided
|
|
97
|
+
#
|
|
98
|
+
# @example Basic streaming
|
|
99
|
+
# MyAgent.stream(query: "test") do |chunk|
|
|
100
|
+
# print chunk.content
|
|
101
|
+
# end
|
|
102
|
+
#
|
|
103
|
+
# @example With result metadata
|
|
104
|
+
# result = MyAgent.stream(query: "test") { |c| print c.content }
|
|
105
|
+
# puts "\nTokens: #{result.total_tokens}"
|
|
106
|
+
def stream(**kwargs, &block)
|
|
107
|
+
raise ArgumentError, "Block required for streaming" unless block_given?
|
|
108
|
+
|
|
109
|
+
instance = new(**kwargs)
|
|
110
|
+
instance.instance_variable_set(:@force_streaming, true)
|
|
111
|
+
instance.call(&block)
|
|
112
|
+
end
|
|
86
113
|
end
|
|
87
114
|
|
|
88
115
|
# @!attribute [r] model
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Manages deprecation warnings with configurable behavior
|
|
6
|
+
#
|
|
7
|
+
# Provides a centralized mechanism for deprecation warnings that can be
|
|
8
|
+
# configured to raise exceptions in test environments or emit warnings
|
|
9
|
+
# in production.
|
|
10
|
+
#
|
|
11
|
+
# @example Emitting a deprecation warning
|
|
12
|
+
# Deprecations.warn("cache(ttl) is deprecated, use cache_for(ttl) instead")
|
|
13
|
+
#
|
|
14
|
+
# @example Enabling strict mode in tests
|
|
15
|
+
# RubyLLM::Agents::Deprecations.raise_on_deprecation = true
|
|
16
|
+
#
|
|
17
|
+
# @api public
|
|
18
|
+
module Deprecations
|
|
19
|
+
# Error raised when deprecation warnings are configured to raise
|
|
20
|
+
#
|
|
21
|
+
# @api public
|
|
22
|
+
class DeprecationError < StandardError; end
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# @!attribute [rw] raise_on_deprecation
|
|
26
|
+
# @return [Boolean] Whether to raise exceptions instead of warnings
|
|
27
|
+
attr_accessor :raise_on_deprecation
|
|
28
|
+
|
|
29
|
+
# @!attribute [rw] silenced
|
|
30
|
+
# @return [Boolean] Whether to silence all deprecation warnings
|
|
31
|
+
attr_accessor :silenced
|
|
32
|
+
|
|
33
|
+
# Emits a deprecation warning or raises an error
|
|
34
|
+
#
|
|
35
|
+
# @param message [String] The deprecation message
|
|
36
|
+
# @param callstack [Array<String>] The call stack (defaults to caller)
|
|
37
|
+
# @return [void]
|
|
38
|
+
# @raise [DeprecationError] If raise_on_deprecation is true
|
|
39
|
+
def warn(message, callstack = caller)
|
|
40
|
+
return if silenced
|
|
41
|
+
|
|
42
|
+
full_message = "[RubyLLM::Agents DEPRECATION] #{message}"
|
|
43
|
+
|
|
44
|
+
if raise_on_deprecation
|
|
45
|
+
raise DeprecationError, full_message
|
|
46
|
+
elsif defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
47
|
+
# Use Rails deprecator if available (Rails 7.1+)
|
|
48
|
+
if Rails.application.respond_to?(:deprecators)
|
|
49
|
+
Rails.application.deprecators[:ruby_llm_agents]&.warn(full_message, callstack) ||
|
|
50
|
+
::Kernel.warn("#{full_message}\n #{callstack.first}")
|
|
51
|
+
else
|
|
52
|
+
::Kernel.warn("#{full_message}\n #{callstack.first}")
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
::Kernel.warn("#{full_message}\n #{callstack.first}")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Temporarily silence deprecation warnings within a block
|
|
60
|
+
#
|
|
61
|
+
# @yield Block to execute with silenced warnings
|
|
62
|
+
# @return [Object] The return value of the block
|
|
63
|
+
def silence
|
|
64
|
+
old_silenced = silenced
|
|
65
|
+
self.silenced = true
|
|
66
|
+
yield
|
|
67
|
+
ensure
|
|
68
|
+
self.silenced = old_silenced
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Reset to defaults
|
|
73
|
+
self.raise_on_deprecation = false
|
|
74
|
+
self.silenced = false
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module Reliability
|
|
6
|
+
# Manages circuit breakers for multiple models
|
|
7
|
+
#
|
|
8
|
+
# Provides centralized access to circuit breakers with
|
|
9
|
+
# multi-tenant support and caching.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# manager = BreakerManager.new("MyAgent", config: { errors: 5, within: 60 })
|
|
13
|
+
# manager.open?("gpt-4o") # => false
|
|
14
|
+
# manager.record_failure!("gpt-4o")
|
|
15
|
+
# manager.record_success!("gpt-4o")
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
18
|
+
class BreakerManager
|
|
19
|
+
# @param agent_type [String] The agent class name
|
|
20
|
+
# @param config [Hash, nil] Circuit breaker configuration
|
|
21
|
+
# @param tenant_id [String, nil] Optional tenant identifier
|
|
22
|
+
def initialize(agent_type, config:, tenant_id: nil)
|
|
23
|
+
@agent_type = agent_type
|
|
24
|
+
@config = config
|
|
25
|
+
@tenant_id = tenant_id
|
|
26
|
+
@breakers = {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Gets or creates a circuit breaker for a model
|
|
30
|
+
#
|
|
31
|
+
# @param model_id [String] Model identifier
|
|
32
|
+
# @return [CircuitBreaker, nil] The circuit breaker or nil if not configured
|
|
33
|
+
def for_model(model_id)
|
|
34
|
+
return nil unless @config
|
|
35
|
+
|
|
36
|
+
@breakers[model_id] ||= CircuitBreaker.from_config(
|
|
37
|
+
@agent_type,
|
|
38
|
+
model_id,
|
|
39
|
+
@config,
|
|
40
|
+
tenant_id: @tenant_id
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Checks if a model's circuit breaker is open
|
|
45
|
+
#
|
|
46
|
+
# @param model_id [String] Model identifier
|
|
47
|
+
# @return [Boolean] true if breaker is open
|
|
48
|
+
def open?(model_id)
|
|
49
|
+
breaker = for_model(model_id)
|
|
50
|
+
breaker&.open? || false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Records a success for a model
|
|
54
|
+
#
|
|
55
|
+
# @param model_id [String] Model identifier
|
|
56
|
+
# @return [void]
|
|
57
|
+
def record_success!(model_id)
|
|
58
|
+
for_model(model_id)&.record_success!
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Records a failure for a model
|
|
62
|
+
#
|
|
63
|
+
# @param model_id [String] Model identifier
|
|
64
|
+
# @return [Boolean] true if breaker is now open
|
|
65
|
+
def record_failure!(model_id)
|
|
66
|
+
breaker = for_model(model_id)
|
|
67
|
+
breaker&.record_failure!
|
|
68
|
+
breaker&.open? || false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Checks if circuit breaker is configured
|
|
72
|
+
#
|
|
73
|
+
# @return [Boolean] true if config present
|
|
74
|
+
def enabled?
|
|
75
|
+
@config.present?
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module Reliability
|
|
6
|
+
# Manages execution constraints like total timeout and budget
|
|
7
|
+
#
|
|
8
|
+
# Tracks elapsed time and enforces timeout limits across
|
|
9
|
+
# all retry and fallback attempts.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# constraints = ExecutionConstraints.new(total_timeout: 30)
|
|
13
|
+
# constraints.timeout_exceeded? # => false
|
|
14
|
+
# constraints.enforce_timeout! # raises if exceeded
|
|
15
|
+
# constraints.elapsed # => 5.2
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
18
|
+
class ExecutionConstraints
|
|
19
|
+
attr_reader :total_timeout, :started_at, :deadline
|
|
20
|
+
|
|
21
|
+
# @param total_timeout [Integer, nil] Total timeout in seconds
|
|
22
|
+
def initialize(total_timeout: nil)
|
|
23
|
+
@total_timeout = total_timeout
|
|
24
|
+
@started_at = Time.current
|
|
25
|
+
@deadline = total_timeout ? @started_at + total_timeout : nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Checks if total timeout has been exceeded
|
|
29
|
+
#
|
|
30
|
+
# @return [Boolean] true if past deadline
|
|
31
|
+
def timeout_exceeded?
|
|
32
|
+
deadline && Time.current > deadline
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns elapsed time since start
|
|
36
|
+
#
|
|
37
|
+
# @return [Float] Elapsed seconds
|
|
38
|
+
def elapsed
|
|
39
|
+
Time.current - started_at
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Raises TotalTimeoutError if timeout exceeded
|
|
43
|
+
#
|
|
44
|
+
# @raise [TotalTimeoutError] If timeout exceeded
|
|
45
|
+
# @return [void]
|
|
46
|
+
def enforce_timeout!
|
|
47
|
+
if timeout_exceeded?
|
|
48
|
+
raise TotalTimeoutError.new(total_timeout, elapsed)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns remaining time until deadline
|
|
53
|
+
#
|
|
54
|
+
# @return [Float, nil] Remaining seconds or nil if no timeout
|
|
55
|
+
def remaining
|
|
56
|
+
return nil unless deadline
|
|
57
|
+
[deadline - Time.current, 0].max
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Checks if there's a timeout configured
|
|
61
|
+
#
|
|
62
|
+
# @return [Boolean] true if timeout is set
|
|
63
|
+
def has_timeout?
|
|
64
|
+
total_timeout.present?
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module Reliability
|
|
6
|
+
# Coordinates reliability features during agent execution
|
|
7
|
+
#
|
|
8
|
+
# Orchestrates retry strategy, fallback routing, circuit breakers,
|
|
9
|
+
# and execution constraints into a cohesive execution flow.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# executor = Executor.new(
|
|
13
|
+
# config: { retries: { max: 3 }, fallback_models: ["gpt-4o-mini"] },
|
|
14
|
+
# primary_model: "gpt-4o",
|
|
15
|
+
# agent_type: "MyAgent"
|
|
16
|
+
# )
|
|
17
|
+
# executor.execute { |model| call_llm(model) }
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
class Executor
|
|
21
|
+
attr_reader :retry_strategy, :fallback_routing, :breaker_manager, :constraints
|
|
22
|
+
|
|
23
|
+
# @param config [Hash] Reliability configuration
|
|
24
|
+
# @param primary_model [String] Primary model identifier
|
|
25
|
+
# @param agent_type [String] Agent class name
|
|
26
|
+
# @param tenant_id [String, nil] Optional tenant identifier
|
|
27
|
+
def initialize(config:, primary_model:, agent_type:, tenant_id: nil)
|
|
28
|
+
retries_config = config[:retries] || {}
|
|
29
|
+
|
|
30
|
+
@retry_strategy = RetryStrategy.new(
|
|
31
|
+
max: retries_config[:max] || 0,
|
|
32
|
+
backoff: retries_config[:backoff] || :exponential,
|
|
33
|
+
base: retries_config[:base] || 0.4,
|
|
34
|
+
max_delay: retries_config[:max_delay] || 3.0,
|
|
35
|
+
on: retries_config[:on] || []
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@fallback_routing = FallbackRouting.new(
|
|
39
|
+
primary_model,
|
|
40
|
+
fallback_models: config[:fallback_models] || []
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@breaker_manager = BreakerManager.new(
|
|
44
|
+
agent_type,
|
|
45
|
+
config: config[:circuit_breaker],
|
|
46
|
+
tenant_id: tenant_id
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@constraints = ExecutionConstraints.new(
|
|
50
|
+
total_timeout: config[:total_timeout]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@last_error = nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns all models that will be tried
|
|
57
|
+
#
|
|
58
|
+
# @return [Array<String>] Model identifiers
|
|
59
|
+
def models_to_try
|
|
60
|
+
fallback_routing.models
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Executes with full reliability support
|
|
64
|
+
#
|
|
65
|
+
# Iterates through models with retries, respecting circuit breakers
|
|
66
|
+
# and timeout constraints.
|
|
67
|
+
#
|
|
68
|
+
# @yield [model] Block to execute with the current model
|
|
69
|
+
# @yieldparam model [String] The model to use for this attempt
|
|
70
|
+
# @return [Object] Result of successful execution
|
|
71
|
+
# @raise [AllModelsExhaustedError] If all models fail
|
|
72
|
+
# @raise [TotalTimeoutError] If total timeout exceeded
|
|
73
|
+
def execute
|
|
74
|
+
until fallback_routing.exhausted?
|
|
75
|
+
model = fallback_routing.current_model
|
|
76
|
+
|
|
77
|
+
# Check circuit breaker
|
|
78
|
+
if breaker_manager.open?(model)
|
|
79
|
+
fallback_routing.advance!
|
|
80
|
+
next
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Try with retries
|
|
84
|
+
result = execute_with_retries(model) { |m| yield(m) }
|
|
85
|
+
return result if result
|
|
86
|
+
|
|
87
|
+
fallback_routing.advance!
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
raise AllModelsExhaustedError.new(
|
|
91
|
+
fallback_routing.models,
|
|
92
|
+
@last_error || StandardError.new("All models failed")
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def execute_with_retries(model)
|
|
99
|
+
attempt_index = 0
|
|
100
|
+
|
|
101
|
+
loop do
|
|
102
|
+
constraints.enforce_timeout!
|
|
103
|
+
|
|
104
|
+
begin
|
|
105
|
+
result = yield(model)
|
|
106
|
+
breaker_manager.record_success!(model)
|
|
107
|
+
return result
|
|
108
|
+
rescue => e
|
|
109
|
+
@last_error = e
|
|
110
|
+
breaker_manager.record_failure!(model)
|
|
111
|
+
|
|
112
|
+
if retry_strategy.retryable?(e) && retry_strategy.should_retry?(attempt_index)
|
|
113
|
+
attempt_index += 1
|
|
114
|
+
sleep(retry_strategy.delay_for(attempt_index))
|
|
115
|
+
else
|
|
116
|
+
return nil # Move to next model
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module Reliability
|
|
6
|
+
# Routes execution through fallback models when primary fails
|
|
7
|
+
#
|
|
8
|
+
# Manages the model fallback chain and tracks which models have been tried.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# routing = FallbackRouting.new("gpt-4o", fallback_models: ["gpt-4o-mini"])
|
|
12
|
+
# routing.current_model # => "gpt-4o"
|
|
13
|
+
# routing.advance! # => "gpt-4o-mini"
|
|
14
|
+
# routing.exhausted? # => false
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
class FallbackRouting
|
|
18
|
+
attr_reader :models
|
|
19
|
+
|
|
20
|
+
# @param primary_model [String] The primary model identifier
|
|
21
|
+
# @param fallback_models [Array<String>] Fallback model identifiers
|
|
22
|
+
def initialize(primary_model, fallback_models: [])
|
|
23
|
+
@models = [primary_model, *fallback_models].uniq
|
|
24
|
+
@current_index = 0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns the current model to try
|
|
28
|
+
#
|
|
29
|
+
# @return [String, nil] Model identifier or nil if exhausted
|
|
30
|
+
def current_model
|
|
31
|
+
models[@current_index]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Advances to the next fallback model
|
|
35
|
+
#
|
|
36
|
+
# @return [String, nil] Next model or nil if exhausted
|
|
37
|
+
def advance!
|
|
38
|
+
@current_index += 1
|
|
39
|
+
current_model
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Checks if more models are available after current
|
|
43
|
+
#
|
|
44
|
+
# @return [Boolean] true if more models to try
|
|
45
|
+
def has_more?
|
|
46
|
+
@current_index < models.length - 1
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Checks if all models have been exhausted
|
|
50
|
+
#
|
|
51
|
+
# @return [Boolean] true if no more models
|
|
52
|
+
def exhausted?
|
|
53
|
+
@current_index >= models.length
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Resets to the first model
|
|
57
|
+
#
|
|
58
|
+
# @return [void]
|
|
59
|
+
def reset!
|
|
60
|
+
@current_index = 0
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns models that have been tried so far
|
|
64
|
+
#
|
|
65
|
+
# @return [Array<String>] Models already attempted
|
|
66
|
+
def tried_models
|
|
67
|
+
models[0..@current_index]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module Reliability
|
|
6
|
+
# Handles retry logic with configurable backoff strategies
|
|
7
|
+
#
|
|
8
|
+
# Provides exponential and constant backoff with jitter,
|
|
9
|
+
# retry counting, and delay calculation.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# strategy = RetryStrategy.new(max: 3, backoff: :exponential, base: 0.4)
|
|
13
|
+
# strategy.should_retry?(attempt_index) # => true/false
|
|
14
|
+
# strategy.delay_for(attempt_index) # => 0.6 (with jitter)
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
class RetryStrategy
|
|
18
|
+
attr_reader :max, :backoff, :base, :max_delay, :custom_errors
|
|
19
|
+
|
|
20
|
+
# @param max [Integer] Maximum retry attempts
|
|
21
|
+
# @param backoff [Symbol] :constant or :exponential
|
|
22
|
+
# @param base [Float] Base delay in seconds
|
|
23
|
+
# @param max_delay [Float] Maximum delay cap
|
|
24
|
+
# @param on [Array<Class>] Additional error classes to retry on
|
|
25
|
+
def initialize(max: 0, backoff: :exponential, base: 0.4, max_delay: 3.0, on: [])
|
|
26
|
+
@max = max
|
|
27
|
+
@backoff = backoff
|
|
28
|
+
@base = base
|
|
29
|
+
@max_delay = max_delay
|
|
30
|
+
@custom_errors = Array(on)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Determines if retry should occur
|
|
34
|
+
#
|
|
35
|
+
# @param attempt_index [Integer] Current attempt number (0-indexed)
|
|
36
|
+
# @return [Boolean] true if should retry
|
|
37
|
+
def should_retry?(attempt_index)
|
|
38
|
+
attempt_index < max
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Calculates delay before next retry
|
|
42
|
+
#
|
|
43
|
+
# @param attempt_index [Integer] Current attempt number
|
|
44
|
+
# @return [Float] Delay in seconds (includes jitter)
|
|
45
|
+
def delay_for(attempt_index)
|
|
46
|
+
base_delay = case backoff
|
|
47
|
+
when :constant
|
|
48
|
+
base
|
|
49
|
+
when :exponential
|
|
50
|
+
[base * (2**attempt_index), max_delay].min
|
|
51
|
+
else
|
|
52
|
+
base
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Add jitter (0-50% of base delay)
|
|
56
|
+
base_delay + (rand * base_delay * 0.5)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Checks if an error is retryable
|
|
60
|
+
#
|
|
61
|
+
# @param error [Exception] The error to check
|
|
62
|
+
# @return [Boolean] true if retryable
|
|
63
|
+
def retryable?(error)
|
|
64
|
+
RubyLLM::Agents::Reliability.retryable_error?(error, custom_errors: custom_errors)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns all retryable error classes
|
|
68
|
+
#
|
|
69
|
+
# @return [Array<Class>] Error classes to retry on
|
|
70
|
+
def retryable_errors
|
|
71
|
+
RubyLLM::Agents::Reliability.default_retryable_errors + custom_errors
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -220,8 +220,78 @@ module RubyLLM
|
|
|
220
220
|
}
|
|
221
221
|
end
|
|
222
222
|
|
|
223
|
-
#
|
|
224
|
-
|
|
223
|
+
# @!group Deprecated Hash Delegation
|
|
224
|
+
#
|
|
225
|
+
# These methods delegate to content for backward compatibility but are deprecated.
|
|
226
|
+
# Use result.content directly instead.
|
|
227
|
+
#
|
|
228
|
+
# @deprecated Access content directly via {#content} instead.
|
|
229
|
+
# These methods will be removed in version 1.0.
|
|
230
|
+
#
|
|
231
|
+
# @example Migration
|
|
232
|
+
# # Before (deprecated)
|
|
233
|
+
# result[:key]
|
|
234
|
+
# result.dig(:nested, :key)
|
|
235
|
+
#
|
|
236
|
+
# # After (recommended)
|
|
237
|
+
# result.content[:key]
|
|
238
|
+
# result.content.dig(:nested, :key)
|
|
239
|
+
|
|
240
|
+
# @deprecated Use result.content[:key] instead
|
|
241
|
+
def [](key)
|
|
242
|
+
RubyLLM::Agents::Deprecations.warn(
|
|
243
|
+
"Result#[] is deprecated. Use result.content[:key] instead.",
|
|
244
|
+
caller
|
|
245
|
+
)
|
|
246
|
+
content&.[](key)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# @deprecated Use result.content.dig(...) instead
|
|
250
|
+
def dig(*keys)
|
|
251
|
+
RubyLLM::Agents::Deprecations.warn(
|
|
252
|
+
"Result#dig is deprecated. Use result.content.dig(...) instead.",
|
|
253
|
+
caller
|
|
254
|
+
)
|
|
255
|
+
content&.dig(*keys)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# @deprecated Use result.content.keys instead
|
|
259
|
+
def keys
|
|
260
|
+
RubyLLM::Agents::Deprecations.warn(
|
|
261
|
+
"Result#keys is deprecated. Use result.content.keys instead.",
|
|
262
|
+
caller
|
|
263
|
+
)
|
|
264
|
+
content&.keys
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# @deprecated Use result.content.values instead
|
|
268
|
+
def values
|
|
269
|
+
RubyLLM::Agents::Deprecations.warn(
|
|
270
|
+
"Result#values is deprecated. Use result.content.values instead.",
|
|
271
|
+
caller
|
|
272
|
+
)
|
|
273
|
+
content&.values
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# @deprecated Use result.content.each instead
|
|
277
|
+
def each(&block)
|
|
278
|
+
RubyLLM::Agents::Deprecations.warn(
|
|
279
|
+
"Result#each is deprecated. Use result.content.each instead.",
|
|
280
|
+
caller
|
|
281
|
+
)
|
|
282
|
+
content&.each(&block)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# @deprecated Use result.content.map instead
|
|
286
|
+
def map(&block)
|
|
287
|
+
RubyLLM::Agents::Deprecations.warn(
|
|
288
|
+
"Result#map is deprecated. Use result.content.map instead.",
|
|
289
|
+
caller
|
|
290
|
+
)
|
|
291
|
+
content&.map(&block)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# @!endgroup
|
|
225
295
|
|
|
226
296
|
# Custom to_json that returns content as JSON for backward compatibility
|
|
227
297
|
#
|
data/lib/ruby_llm/agents.rb
CHANGED
|
@@ -5,7 +5,13 @@ require "ruby_llm"
|
|
|
5
5
|
|
|
6
6
|
require_relative "agents/version"
|
|
7
7
|
require_relative "agents/configuration"
|
|
8
|
+
require_relative "agents/deprecations"
|
|
8
9
|
require_relative "agents/reliability"
|
|
10
|
+
require_relative "agents/reliability/retry_strategy"
|
|
11
|
+
require_relative "agents/reliability/fallback_routing"
|
|
12
|
+
require_relative "agents/reliability/breaker_manager"
|
|
13
|
+
require_relative "agents/reliability/execution_constraints"
|
|
14
|
+
require_relative "agents/reliability/executor"
|
|
9
15
|
require_relative "agents/redactor"
|
|
10
16
|
require_relative "agents/circuit_breaker"
|
|
11
17
|
require_relative "agents/budget_tracker"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_llm-agents
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- adham90
|
|
@@ -140,6 +140,7 @@ files:
|
|
|
140
140
|
- lib/ruby_llm/agents/base/cost_calculation.rb
|
|
141
141
|
- lib/ruby_llm/agents/base/dsl.rb
|
|
142
142
|
- lib/ruby_llm/agents/base/execution.rb
|
|
143
|
+
- lib/ruby_llm/agents/base/reliability_dsl.rb
|
|
143
144
|
- lib/ruby_llm/agents/base/reliability_execution.rb
|
|
144
145
|
- lib/ruby_llm/agents/base/response_building.rb
|
|
145
146
|
- lib/ruby_llm/agents/base/tool_tracking.rb
|
|
@@ -147,12 +148,18 @@ files:
|
|
|
147
148
|
- lib/ruby_llm/agents/cache_helper.rb
|
|
148
149
|
- lib/ruby_llm/agents/circuit_breaker.rb
|
|
149
150
|
- lib/ruby_llm/agents/configuration.rb
|
|
151
|
+
- lib/ruby_llm/agents/deprecations.rb
|
|
150
152
|
- lib/ruby_llm/agents/engine.rb
|
|
151
153
|
- lib/ruby_llm/agents/execution_logger_job.rb
|
|
152
154
|
- lib/ruby_llm/agents/inflections.rb
|
|
153
155
|
- lib/ruby_llm/agents/instrumentation.rb
|
|
154
156
|
- lib/ruby_llm/agents/redactor.rb
|
|
155
157
|
- lib/ruby_llm/agents/reliability.rb
|
|
158
|
+
- lib/ruby_llm/agents/reliability/breaker_manager.rb
|
|
159
|
+
- lib/ruby_llm/agents/reliability/execution_constraints.rb
|
|
160
|
+
- lib/ruby_llm/agents/reliability/executor.rb
|
|
161
|
+
- lib/ruby_llm/agents/reliability/fallback_routing.rb
|
|
162
|
+
- lib/ruby_llm/agents/reliability/retry_strategy.rb
|
|
156
163
|
- lib/ruby_llm/agents/result.rb
|
|
157
164
|
- lib/ruby_llm/agents/version.rb
|
|
158
165
|
- lib/ruby_llm/agents/workflow.rb
|