ruby_llm-agents 3.5.5 → 3.7.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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -0
  3. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +155 -10
  4. data/app/controllers/ruby_llm/agents/executions_controller.rb +1 -3
  5. data/app/helpers/ruby_llm/agents/application_helper.rb +15 -28
  6. data/app/models/ruby_llm/agents/execution/replayable.rb +124 -0
  7. data/app/models/ruby_llm/agents/execution/scopes.rb +42 -1
  8. data/app/models/ruby_llm/agents/execution.rb +50 -1
  9. data/app/models/ruby_llm/agents/tenant/budgetable.rb +28 -4
  10. data/app/views/layouts/ruby_llm/agents/application.html.erb +41 -28
  11. data/app/views/ruby_llm/agents/agents/show.html.erb +16 -1
  12. data/app/views/ruby_llm/agents/dashboard/_top_tenants.html.erb +47 -0
  13. data/app/views/ruby_llm/agents/dashboard/index.html.erb +404 -107
  14. data/app/views/ruby_llm/agents/system_config/show.html.erb +0 -13
  15. data/lib/generators/ruby_llm_agents/rename_agent_generator.rb +53 -0
  16. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +0 -15
  17. data/lib/generators/ruby_llm_agents/templates/rename_agent_migration.rb.tt +19 -0
  18. data/lib/ruby_llm/agents/agent_tool.rb +125 -0
  19. data/lib/ruby_llm/agents/audio/speaker.rb +5 -3
  20. data/lib/ruby_llm/agents/audio/speech_pricing.rb +63 -187
  21. data/lib/ruby_llm/agents/audio/transcriber.rb +5 -3
  22. data/lib/ruby_llm/agents/audio/transcription_pricing.rb +5 -7
  23. data/lib/ruby_llm/agents/base_agent.rb +144 -5
  24. data/lib/ruby_llm/agents/core/configuration.rb +178 -53
  25. data/lib/ruby_llm/agents/core/errors.rb +3 -77
  26. data/lib/ruby_llm/agents/core/instrumentation.rb +0 -17
  27. data/lib/ruby_llm/agents/core/version.rb +1 -1
  28. data/lib/ruby_llm/agents/dsl/base.rb +0 -8
  29. data/lib/ruby_llm/agents/dsl/queryable.rb +124 -0
  30. data/lib/ruby_llm/agents/dsl.rb +1 -0
  31. data/lib/ruby_llm/agents/eval/eval_result.rb +73 -0
  32. data/lib/ruby_llm/agents/eval/eval_run.rb +124 -0
  33. data/lib/ruby_llm/agents/eval/eval_suite.rb +264 -0
  34. data/lib/ruby_llm/agents/eval.rb +5 -0
  35. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -1
  36. data/lib/ruby_llm/agents/image/generator/pricing.rb +75 -217
  37. data/lib/ruby_llm/agents/image/generator.rb +5 -3
  38. data/lib/ruby_llm/agents/infrastructure/attempt_tracker.rb +8 -0
  39. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +4 -2
  40. data/lib/ruby_llm/agents/pipeline/builder.rb +43 -0
  41. data/lib/ruby_llm/agents/pipeline/context.rb +11 -1
  42. data/lib/ruby_llm/agents/pipeline/executor.rb +1 -25
  43. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +26 -1
  44. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +18 -0
  45. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +90 -0
  46. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +29 -0
  47. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +11 -4
  48. data/lib/ruby_llm/agents/pipeline.rb +0 -92
  49. data/lib/ruby_llm/agents/results/background_removal_result.rb +11 -1
  50. data/lib/ruby_llm/agents/results/base.rb +23 -1
  51. data/lib/ruby_llm/agents/results/embedding_result.rb +14 -1
  52. data/lib/ruby_llm/agents/results/image_analysis_result.rb +11 -1
  53. data/lib/ruby_llm/agents/results/image_edit_result.rb +11 -1
  54. data/lib/ruby_llm/agents/results/image_generation_result.rb +12 -3
  55. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +11 -1
  56. data/lib/ruby_llm/agents/results/image_transform_result.rb +11 -1
  57. data/lib/ruby_llm/agents/results/image_upscale_result.rb +11 -1
  58. data/lib/ruby_llm/agents/results/image_variation_result.rb +11 -1
  59. data/lib/ruby_llm/agents/results/speech_result.rb +20 -1
  60. data/lib/ruby_llm/agents/results/transcription_result.rb +20 -1
  61. data/lib/ruby_llm/agents/text/embedder.rb +23 -18
  62. data/lib/ruby_llm/agents.rb +73 -5
  63. data/lib/tasks/ruby_llm_agents.rake +21 -0
  64. metadata +11 -6
  65. data/lib/ruby_llm/agents/infrastructure/reliability/breaker_manager.rb +0 -80
  66. data/lib/ruby_llm/agents/infrastructure/reliability/execution_constraints.rb +0 -69
  67. data/lib/ruby_llm/agents/infrastructure/reliability/executor.rb +0 -125
  68. data/lib/ruby_llm/agents/infrastructure/reliability/fallback_routing.rb +0 -72
  69. data/lib/ruby_llm/agents/infrastructure/reliability/retry_strategy.rb +0 -82
@@ -151,6 +151,14 @@ module RubyLLM
151
151
 
152
152
  # @!endgroup
153
153
 
154
+ # @!group Execution Record
155
+
156
+ # @!attribute [r] execution_id
157
+ # @return [Integer, nil] Database ID of the associated Execution record
158
+ attr_reader :execution_id
159
+
160
+ # @!endgroup
161
+
154
162
  # Creates a new SpeechResult instance
155
163
  #
156
164
  # @param attributes [Hash] Result attributes
@@ -218,6 +226,16 @@ module RubyLLM
218
226
  # Error
219
227
  @error_class = attributes[:error_class]
220
228
  @error_message = attributes[:error_message]
229
+
230
+ # Execution record
231
+ @execution_id = attributes[:execution_id]
232
+ end
233
+
234
+ # Loads the associated Execution record from the database
235
+ #
236
+ # @return [RubyLLM::Agents::Execution, nil] The execution record, or nil
237
+ def execution
238
+ @execution ||= RubyLLM::Agents::Execution.find_by(id: execution_id) if execution_id
221
239
  end
222
240
 
223
241
  # Returns whether the speech generation succeeded
@@ -303,7 +321,8 @@ module RubyLLM
303
321
  status: status,
304
322
  tenant_id: tenant_id,
305
323
  error_class: error_class,
306
- error_message: error_message
324
+ error_message: error_message,
325
+ execution_id: execution_id
307
326
  # Note: audio binary data excluded for serialization safety
308
327
  }
309
328
  end
@@ -166,6 +166,14 @@ module RubyLLM
166
166
 
167
167
  # @!endgroup
168
168
 
169
+ # @!group Execution Record
170
+
171
+ # @!attribute [r] execution_id
172
+ # @return [Integer, nil] Database ID of the associated Execution record
173
+ attr_reader :execution_id
174
+
175
+ # @!endgroup
176
+
169
177
  # Creates a new TranscriptionResult instance
170
178
  #
171
179
  # @param attributes [Hash] Result attributes
@@ -239,6 +247,16 @@ module RubyLLM
239
247
  # Error
240
248
  @error_class = attributes[:error_class]
241
249
  @error_message = attributes[:error_message]
250
+
251
+ # Execution record
252
+ @execution_id = attributes[:execution_id]
253
+ end
254
+
255
+ # Loads the associated Execution record from the database
256
+ #
257
+ # @return [RubyLLM::Agents::Execution, nil] The execution record, or nil
258
+ def execution
259
+ @execution ||= RubyLLM::Agents::Execution.find_by(id: execution_id) if execution_id
242
260
  end
243
261
 
244
262
  # Returns whether the transcription succeeded
@@ -368,7 +386,8 @@ module RubyLLM
368
386
  status: status,
369
387
  tenant_id: tenant_id,
370
388
  error_class: error_class,
371
- error_message: error_message
389
+ error_message: error_message,
390
+ execution_id: execution_id
372
391
  }
373
392
  end
374
393
 
@@ -245,7 +245,8 @@ module RubyLLM
245
245
  started_at: context.started_at || execution_started_at,
246
246
  completed_at: execution_completed_at,
247
247
  duration_ms: duration_ms,
248
- tenant_id: context.tenant_id
248
+ tenant_id: context.tenant_id,
249
+ execution_id: context.execution_id
249
250
  )
250
251
  end
251
252
 
@@ -377,7 +378,7 @@ module RubyLLM
377
378
  # @param duration_ms [Integer] Execution duration in ms
378
379
  # @param tenant_id [String, nil] Tenant identifier
379
380
  # @return [EmbeddingResult] The final result
380
- def build_result(vectors:, input_tokens:, total_cost:, count:, started_at:, completed_at:, duration_ms:, tenant_id:)
381
+ def build_result(vectors:, input_tokens:, total_cost:, count:, started_at:, completed_at:, duration_ms:, tenant_id:, execution_id: nil)
381
382
  EmbeddingResult.new(
382
383
  vectors: vectors,
383
384
  model_id: resolved_model,
@@ -388,7 +389,8 @@ module RubyLLM
388
389
  count: count,
389
390
  started_at: started_at,
390
391
  completed_at: completed_at,
391
- tenant_id: tenant_id
392
+ tenant_id: tenant_id,
393
+ execution_id: execution_id
392
394
  )
393
395
  end
394
396
 
@@ -397,25 +399,28 @@ module RubyLLM
397
399
  # @param response [Object] The ruby_llm embedding response
398
400
  # @return [Float] Cost in USD
399
401
  def calculate_cost(response)
400
- # ruby_llm may provide cost directly, otherwise estimate
402
+ # ruby_llm may provide cost directly
401
403
  return response.input_cost if response.respond_to?(:input_cost) && response.input_cost
402
404
 
403
- # Fallback: estimate based on tokens and model
405
+ # Look up pricing from the model registry (same approach as BaseAgent)
404
406
  tokens = response.input_tokens || 0
405
- model_name = response.model.to_s
406
-
407
- price_per_million = case model_name
408
- when /text-embedding-3-small/
409
- 0.02
410
- when /text-embedding-3-large/
411
- 0.13
412
- when /text-embedding-ada/
413
- 0.10
414
- else
415
- 0.02 # Default to small pricing
416
- end
407
+ model_id = response.model.to_s
408
+
409
+ input_price = find_embedding_price(model_id)
410
+ (tokens / 1_000_000.0) * input_price
411
+ end
417
412
 
418
- (tokens / 1_000_000.0) * price_per_million
413
+ # Looks up the per-million-token input price for an embedding model
414
+ #
415
+ # @param model_id [String] The model identifier
416
+ # @return [Numeric] Price per million tokens (0 if unknown)
417
+ def find_embedding_price(model_id)
418
+ return 0 unless defined?(RubyLLM::Models)
419
+
420
+ model_info = RubyLLM::Models.find(model_id)
421
+ model_info&.pricing&.text_tokens&.input || 0
422
+ rescue
423
+ 0
419
424
  end
420
425
 
421
426
  # Resolves the model to use
@@ -13,11 +13,6 @@ require_relative "agents/core/llm_tenant"
13
13
 
14
14
  # Infrastructure - Reliability
15
15
  require_relative "agents/infrastructure/reliability"
16
- require_relative "agents/infrastructure/reliability/retry_strategy"
17
- require_relative "agents/infrastructure/reliability/fallback_routing"
18
- require_relative "agents/infrastructure/reliability/breaker_manager"
19
- require_relative "agents/infrastructure/reliability/execution_constraints"
20
- require_relative "agents/infrastructure/reliability/executor"
21
16
 
22
17
  # Pipeline infrastructure (middleware-based execution)
23
18
  require_relative "agents/pipeline"
@@ -28,6 +23,9 @@ require_relative "agents/dsl"
28
23
  # BaseAgent - new middleware-based agent architecture
29
24
  require_relative "agents/base_agent"
30
25
 
26
+ # Agent-as-Tool adapter
27
+ require_relative "agents/agent_tool"
28
+
31
29
  # Infrastructure - Budget & Utilities
32
30
  require_relative "agents/infrastructure/circuit_breaker"
33
31
  require_relative "agents/infrastructure/budget_tracker"
@@ -77,6 +75,9 @@ require_relative "agents/image/analyzer"
77
75
  require_relative "agents/image/background_remover"
78
76
  require_relative "agents/image/pipeline"
79
77
 
78
+ # Evaluation framework
79
+ require_relative "agents/eval"
80
+
80
81
  # Rails integration
81
82
  if defined?(Rails)
82
83
  require_relative "agents/core/inflections"
@@ -142,6 +143,73 @@ module RubyLLM
142
143
  def reset_configuration!
143
144
  @configuration = Configuration.new
144
145
  end
146
+
147
+ # Renames an agent in the database, updating execution records and
148
+ # tenant budget configuration keys
149
+ #
150
+ # @param old_name [String] The previous agent class name
151
+ # @param to [String] The new agent class name
152
+ # @param dry_run [Boolean] If true, returns counts without modifying data
153
+ # @return [Hash] Summary of affected records
154
+ #
155
+ # @example Rename an agent
156
+ # RubyLLM::Agents.rename_agent("CustomerSupportAgent", to: "SupportBot")
157
+ # # => { executions_updated: 1432, tenants_updated: 3 }
158
+ #
159
+ # @example Dry run first
160
+ # RubyLLM::Agents.rename_agent("CustomerSupportAgent", to: "SupportBot", dry_run: true)
161
+ # # => { executions_affected: 1432, tenants_affected: 3 }
162
+ def rename_agent(old_name, to:, dry_run: false)
163
+ old_name = old_name.to_s
164
+ new_name = to.to_s
165
+
166
+ raise ArgumentError, "old_name and new name must be different" if old_name == new_name
167
+ raise ArgumentError, "old_name cannot be blank" if old_name.blank?
168
+ raise ArgumentError, "new name cannot be blank" if new_name.blank?
169
+
170
+ execution_scope = Execution.where(agent_type: old_name)
171
+ execution_count = execution_scope.count
172
+
173
+ tenant_count = 0
174
+ if defined?(Tenant) && Tenant.table_exists?
175
+ Tenant.find_each do |tenant|
176
+ changed = false
177
+ %w[per_agent_daily per_agent_monthly].each do |field|
178
+ hash = tenant.send(field)
179
+ next unless hash.is_a?(Hash) && hash.key?(old_name)
180
+ changed = true
181
+ break
182
+ end
183
+ tenant_count += 1 if changed
184
+ end
185
+ end
186
+
187
+ if dry_run
188
+ {executions_affected: execution_count, tenants_affected: tenant_count}
189
+ else
190
+ executions_updated = execution_scope.update_all(agent_type: new_name)
191
+
192
+ tenants_updated = 0
193
+ if defined?(Tenant) && Tenant.table_exists?
194
+ Tenant.find_each do |tenant|
195
+ changed = false
196
+ %w[per_agent_daily per_agent_monthly].each do |field|
197
+ hash = tenant.send(field)
198
+ next unless hash.is_a?(Hash) && hash.key?(old_name)
199
+ hash[new_name] = hash.delete(old_name)
200
+ tenant.send(:"#{field}=", hash)
201
+ changed = true
202
+ end
203
+ if changed
204
+ tenant.save!
205
+ tenants_updated += 1
206
+ end
207
+ end
208
+ end
209
+
210
+ {executions_updated: executions_updated, tenants_updated: tenants_updated}
211
+ end
212
+ end
145
213
  end
146
214
  end
147
215
  end
@@ -1,6 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  namespace :ruby_llm_agents do
4
+ desc "Rename an agent type in execution records. Usage: rake ruby_llm_agents:rename_agent FROM=OldName TO=NewName [DRY_RUN=1]"
5
+ task rename_agent: :environment do
6
+ from = ENV["FROM"]
7
+ to = ENV["TO"]
8
+ dry_run = ENV["DRY_RUN"] == "1"
9
+
10
+ abort "Usage: rake ruby_llm_agents:rename_agent FROM=OldAgentName TO=NewAgentName [DRY_RUN=1]" unless from && to
11
+
12
+ result = RubyLLM::Agents.rename_agent(from, to: to, dry_run: dry_run)
13
+
14
+ if dry_run
15
+ puts "Dry run results:"
16
+ puts " Executions affected: #{result[:executions_affected]}"
17
+ puts " Tenants affected: #{result[:tenants_affected]}"
18
+ else
19
+ puts "Rename complete:"
20
+ puts " Executions updated: #{result[:executions_updated]}"
21
+ puts " Tenants updated: #{result[:tenants_updated]}"
22
+ end
23
+ end
24
+
4
25
  namespace :tenants do
5
26
  desc "Refresh all tenant counters from executions table"
6
27
  task refresh: :environment do
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: 3.5.5
4
+ version: 3.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - adham90
@@ -88,6 +88,7 @@ files:
88
88
  - app/models/ruby_llm/agents/execution.rb
89
89
  - app/models/ruby_llm/agents/execution/analytics.rb
90
90
  - app/models/ruby_llm/agents/execution/metrics.rb
91
+ - app/models/ruby_llm/agents/execution/replayable.rb
91
92
  - app/models/ruby_llm/agents/execution/scopes.rb
92
93
  - app/models/ruby_llm/agents/execution_detail.rb
93
94
  - app/models/ruby_llm/agents/tenant.rb
@@ -111,6 +112,7 @@ files:
111
112
  - app/views/ruby_llm/agents/agents/show.html.erb
112
113
  - app/views/ruby_llm/agents/dashboard/_action_center.html.erb
113
114
  - app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb
115
+ - app/views/ruby_llm/agents/dashboard/_top_tenants.html.erb
114
116
  - app/views/ruby_llm/agents/dashboard/index.html.erb
115
117
  - app/views/ruby_llm/agents/executions/_audio_player.html.erb
116
118
  - app/views/ruby_llm/agents/executions/_execution.html.erb
@@ -146,6 +148,7 @@ files:
146
148
  - lib/generators/ruby_llm_agents/install_generator.rb
147
149
  - lib/generators/ruby_llm_agents/migrate_structure_generator.rb
148
150
  - lib/generators/ruby_llm_agents/multi_tenancy_generator.rb
151
+ - lib/generators/ruby_llm_agents/rename_agent_generator.rb
149
152
  - lib/generators/ruby_llm_agents/restructure_generator.rb
150
153
  - lib/generators/ruby_llm_agents/speaker_generator.rb
151
154
  - lib/generators/ruby_llm_agents/templates/add_assistant_prompt_migration.rb.tt
@@ -189,6 +192,7 @@ files:
189
192
  - lib/generators/ruby_llm_agents/templates/migration.rb.tt
190
193
  - lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt
191
194
  - lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt
195
+ - lib/generators/ruby_llm_agents/templates/rename_agent_migration.rb.tt
192
196
  - lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt
193
197
  - lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt
194
198
  - lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt
@@ -210,6 +214,7 @@ files:
210
214
  - lib/generators/ruby_llm_agents/upgrade_generator.rb
211
215
  - lib/ruby_llm-agents.rb
212
216
  - lib/ruby_llm/agents.rb
217
+ - lib/ruby_llm/agents/agent_tool.rb
213
218
  - lib/ruby_llm/agents/audio/elevenlabs/model_registry.rb
214
219
  - lib/ruby_llm/agents/audio/speaker.rb
215
220
  - lib/ruby_llm/agents/audio/speaker/active_storage_support.rb
@@ -230,7 +235,12 @@ files:
230
235
  - lib/ruby_llm/agents/dsl.rb
231
236
  - lib/ruby_llm/agents/dsl/base.rb
232
237
  - lib/ruby_llm/agents/dsl/caching.rb
238
+ - lib/ruby_llm/agents/dsl/queryable.rb
233
239
  - lib/ruby_llm/agents/dsl/reliability.rb
240
+ - lib/ruby_llm/agents/eval.rb
241
+ - lib/ruby_llm/agents/eval/eval_result.rb
242
+ - lib/ruby_llm/agents/eval/eval_run.rb
243
+ - lib/ruby_llm/agents/eval/eval_suite.rb
234
244
  - lib/ruby_llm/agents/image/analyzer.rb
235
245
  - lib/ruby_llm/agents/image/analyzer/dsl.rb
236
246
  - lib/ruby_llm/agents/image/analyzer/execution.rb
@@ -269,11 +279,6 @@ files:
269
279
  - lib/ruby_llm/agents/infrastructure/circuit_breaker.rb
270
280
  - lib/ruby_llm/agents/infrastructure/execution_logger_job.rb
271
281
  - lib/ruby_llm/agents/infrastructure/reliability.rb
272
- - lib/ruby_llm/agents/infrastructure/reliability/breaker_manager.rb
273
- - lib/ruby_llm/agents/infrastructure/reliability/execution_constraints.rb
274
- - lib/ruby_llm/agents/infrastructure/reliability/executor.rb
275
- - lib/ruby_llm/agents/infrastructure/reliability/fallback_routing.rb
276
- - lib/ruby_llm/agents/infrastructure/reliability/retry_strategy.rb
277
282
  - lib/ruby_llm/agents/pipeline.rb
278
283
  - lib/ruby_llm/agents/pipeline/builder.rb
279
284
  - lib/ruby_llm/agents/pipeline/context.rb
@@ -1,80 +0,0 @@
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
@@ -1,69 +0,0 @@
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
@@ -1,125 +0,0 @@
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
- patterns: config[:retryable_patterns]
37
- )
38
-
39
- @fallback_routing = FallbackRouting.new(
40
- primary_model,
41
- fallback_models: config[:fallback_models] || []
42
- )
43
-
44
- @breaker_manager = BreakerManager.new(
45
- agent_type,
46
- config: config[:circuit_breaker],
47
- tenant_id: tenant_id
48
- )
49
-
50
- @constraints = ExecutionConstraints.new(
51
- total_timeout: config[:total_timeout]
52
- )
53
-
54
- @last_error = nil
55
- end
56
-
57
- # Returns all models that will be tried
58
- #
59
- # @return [Array<String>] Model identifiers
60
- def models_to_try
61
- fallback_routing.models
62
- end
63
-
64
- # Executes with full reliability support
65
- #
66
- # Iterates through models with retries, respecting circuit breakers
67
- # and timeout constraints.
68
- #
69
- # @yield [model] Block to execute with the current model
70
- # @yieldparam model [String] The model to use for this attempt
71
- # @return [Object] Result of successful execution
72
- # @raise [AllModelsExhaustedError] If all models fail
73
- # @raise [TotalTimeoutError] If total timeout exceeded
74
- def execute
75
- until fallback_routing.exhausted?
76
- model = fallback_routing.current_model
77
-
78
- # Check circuit breaker
79
- if breaker_manager.open?(model)
80
- fallback_routing.advance!
81
- next
82
- end
83
-
84
- # Try with retries
85
- result = execute_with_retries(model) { |m| yield(m) }
86
- return result if result
87
-
88
- fallback_routing.advance!
89
- end
90
-
91
- raise AllModelsExhaustedError.new(
92
- fallback_routing.models,
93
- @last_error || StandardError.new("All models failed")
94
- )
95
- end
96
-
97
- private
98
-
99
- def execute_with_retries(model)
100
- attempt_index = 0
101
-
102
- loop do
103
- constraints.enforce_timeout!
104
-
105
- begin
106
- result = yield(model)
107
- breaker_manager.record_success!(model)
108
- return result
109
- rescue => e
110
- @last_error = e
111
- breaker_manager.record_failure!(model)
112
-
113
- if retry_strategy.retryable?(e) && retry_strategy.should_retry?(attempt_index)
114
- attempt_index += 1
115
- sleep(retry_strategy.delay_for(attempt_index))
116
- else
117
- return nil # Move to next model
118
- end
119
- end
120
- end
121
- end
122
- end
123
- end
124
- end
125
- end