activeagent 0.5.1 → 0.6.0rc1

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: ae6f20cf300442f42f9bc00eefea0b8b987dd7e3852e410c685b1f51ba5dcdc2
4
- data.tar.gz: aaabea49a2e71c68c4b8a9b8769fd541b2fad12e1fc16d90aad2219cd0775522
3
+ metadata.gz: 01b5ef5633bece3cdce9d9dba58e499b48c57d80c3a461ceda58adbff738e36a
4
+ data.tar.gz: 9cc9065b10e9bbf0f8fdeba420fe7b9518b7e97f2d32e3bf11af55babd697009
5
5
  SHA512:
6
- metadata.gz: 454f25de89fd2ba05d31e8a39304d0d6a25e6cf7ad3694464ee9013e0f946ca3794500a889d75e77da3d0c1433a084557caf70765b471eb3a6e9324ef78d8853
7
- data.tar.gz: 72075033b38ac50b840e6d6a944922ae2402ab1fdb608f458b9e274883b1ff65286a7eec0530d0a4327c77bf2f1d01f3d471bc18ac986eaee7f9c1ad463816f7
6
+ metadata.gz: 6849b7f94803261d8049ed092aaa8176003b85d5353202aa1a96160860bb07dd98fc9171308c5ed4ea3c9644b6aa68cf09c9c5c827ec7a45300eb14b44e9f1e0
7
+ data.tar.gz: db67a92dd9f9344fd50234caae1641bb70d57ca61200bbea98a177a175c791de886ee3ac42bc807e0e824d95c94adb6e7765908fec3f50cfc4f73aeb66e21c4f
@@ -199,9 +199,9 @@ module ActiveAgent
199
199
  # Add embedding capability to Message class
200
200
  ActiveAgent::ActionPrompt::Message.class_eval do
201
201
  def embed
202
- agent_class = ActiveAgent::Base.descendants.first
202
+ agent_class = ApplicationAgent
203
203
  agent = agent_class.new
204
- agent.context = ActiveAgent::ActionPrompt::Prompt.new(message: self)
204
+ agent.context = ActiveAgent::ActionPrompt::Prompt.new(message: self, agent_instance: agent)
205
205
  agent.embed
206
206
  self
207
207
  end
@@ -217,8 +217,18 @@ module ActiveAgent
217
217
 
218
218
  def handle_response(response)
219
219
  return response unless response.message.requested_actions.present?
220
+
221
+ # Perform the requested actions
220
222
  perform_actions(requested_actions: response.message.requested_actions)
221
- update_context(response)
223
+
224
+ # Continue generation with updated context
225
+ continue_generation
226
+ end
227
+
228
+ def continue_generation
229
+ # Continue generating with the updated context that includes tool results
230
+ generation_provider.generate(context) if context && generation_provider
231
+ handle_response(generation_provider.response)
222
232
  end
223
233
 
224
234
  def update_context(response)
@@ -233,9 +243,12 @@ module ActiveAgent
233
243
 
234
244
  def perform_action(action)
235
245
  current_context = context.clone
236
- # Set params from the action for controller access
246
+ # Merge action params with original params to preserve context
247
+ original_params = current_context.params || {}
237
248
  if action.params.is_a?(Hash)
238
- self.params = action.params
249
+ self.params = original_params.merge(action.params)
250
+ else
251
+ self.params = original_params
239
252
  end
240
253
  process(action.name)
241
254
  context.message.role = :tool
@@ -250,7 +263,7 @@ module ActiveAgent
250
263
  def initialize # :nodoc:
251
264
  super
252
265
  @_prompt_was_called = false
253
- @_context = ActiveAgent::ActionPrompt::Prompt.new(options: self.class.options || {})
266
+ @_context = ActiveAgent::ActionPrompt::Prompt.new(options: self.class.options || {}, agent_instance: self)
254
267
  end
255
268
 
256
269
  def process(method_name, *args) # :nodoc:
@@ -262,7 +275,7 @@ module ActiveAgent
262
275
 
263
276
  ActiveSupport::Notifications.instrument("process.active_agent", payload) do
264
277
  super
265
- @_context = ActiveAgent::ActionPrompt::Prompt.new unless @_prompt_was_called
278
+ @_context = ActiveAgent::ActionPrompt::Prompt.new(agent_instance: self) unless @_prompt_was_called
266
279
  end
267
280
  end
268
281
  ruby2_keywords(:process)
@@ -317,11 +330,12 @@ module ActiveAgent
317
330
 
318
331
  headers = apply_defaults(headers)
319
332
  context.messages = headers[:messages] || []
333
+ context.mcp_servers = headers[:mcp_servers] || []
320
334
  context.context_id = headers[:context_id]
321
335
  context.params = params
322
336
  context.action_name = action_name
323
337
 
324
- context.output_schema = load_schema(headers[:output_schema], set_prefixes(headers[:output_schema], lookup_context.prefixes))
338
+ context.output_schema = render_schema(headers[:output_schema], set_prefixes(headers[:output_schema], lookup_context.prefixes))
325
339
 
326
340
  context.charset = charset = headers[:charset]
327
341
 
@@ -350,7 +364,7 @@ module ActiveAgent
350
364
  prefixes = set_prefixes(action_name, lookup_context.prefixes)
351
365
 
352
366
  action_methods.map do |action|
353
- load_schema(action, prefixes)
367
+ render_schema(action, prefixes)
354
368
  end.compact
355
369
  end
356
370
 
@@ -388,10 +402,14 @@ module ActiveAgent
388
402
  prefixes = lookup_context.prefixes | [ self.class.agent_name ]
389
403
  end
390
404
 
391
- def load_schema(action_name, prefixes)
392
- return unless lookup_context.template_exists?(action_name, prefixes, false, formats: [ :json ])
405
+ def render_schema(schema_or_action, prefixes)
406
+ # If it's already a hash (direct schema), return it
407
+ return schema_or_action if schema_or_action.is_a?(Hash)
408
+
409
+ # Otherwise try to load from template
410
+ return unless lookup_context.template_exists?(schema_or_action, prefixes, false, formats: [ :json ])
393
411
 
394
- JSON.parse render_to_string(locals: { action_name: action_name }, action: action_name, formats: :json)
412
+ JSON.parse render_to_string(locals: { action_name: schema_or_action }, action: schema_or_action, formats: :json)
395
413
  end
396
414
 
397
415
  def merge_options(prompt_options)
@@ -403,7 +421,7 @@ module ActiveAgent
403
421
  # Extract runtime options from prompt_options (exclude instructions as it has special template logic)
404
422
  runtime_options = prompt_options.slice(
405
423
  :model, :temperature, :max_tokens, :stream, :top_p, :frequency_penalty,
406
- :presence_penalty, :response_format, :seed, :stop, :tools_choice
424
+ :presence_penalty, :response_format, :seed, :stop, :tools_choice, :data_collection
407
425
  )
408
426
  # Handle explicit options parameter
409
427
  explicit_options = prompt_options[:options] || {}
@@ -4,12 +4,13 @@ module ActiveAgent
4
4
  module ActionPrompt
5
5
  class Prompt
6
6
  attr_reader :messages, :instructions
7
- attr_accessor :actions, :body, :content_type, :context_id, :message, :options, :mime_version, :charset, :context, :parts, :params, :action_choice, :agent_class, :output_schema, :action_name
7
+ attr_accessor :actions, :body, :content_type, :context_id, :message, :options, :mime_version, :charset, :context, :parts, :params, :action_choice, :agent_class, :output_schema, :action_name, :agent_instance, :mcp_servers
8
8
 
9
9
  def initialize(attributes = {})
10
10
  @options = attributes.fetch(:options, {})
11
11
  @multimodal = attributes.fetch(:multimodal, false)
12
12
  @agent_class = attributes.fetch(:agent_class, ApplicationAgent)
13
+ @agent_instance = attributes.fetch(:agent_instance, nil)
13
14
  @actions = attributes.fetch(:actions, [])
14
15
  @action_choice = attributes.fetch(:action_choice, "")
15
16
  @instructions = attributes.fetch(:instructions, "")
@@ -27,6 +28,7 @@ module ActiveAgent
27
28
  @output_schema = attributes.fetch(:output_schema, nil)
28
29
  @messages = Message.from_messages(@messages)
29
30
  @action_name = attributes.fetch(:action_name, nil)
31
+ @mcp_servers = attributes.fetch(:mcp_servers, [])
30
32
  set_message if attributes[:message].is_a?(String) || @body.is_a?(String) && @message&.content
31
33
  set_messages if @instructions.present?
32
34
  end
@@ -81,7 +83,7 @@ module ActiveAgent
81
83
 
82
84
  def inspect
83
85
  "#<#{self.class}:0x#{object_id.to_s(16)}\n" +
84
- " @options=#{@options.inspect.gsub(Rails.application.credentials.dig(:openai, :api_key), '<OPENAI_API_KEY>')}\n" +
86
+ " @options=#{ActiveAgent.sanitize_credentials(@options.inspect)}\n" +
85
87
  " @actions=#{@actions.inspect}\n" +
86
88
  " @action_choice=#{@action_choice.inspect}\n" +
87
89
  " @instructions=#{@instructions.inspect}\n" +
@@ -37,7 +37,8 @@ module ActiveAgent
37
37
  messages: params[:messages],
38
38
  message: params[:message],
39
39
  context_id: params[:context_id],
40
- options: params[:options]
40
+ options: params[:options],
41
+ mcp_servers: params[:mcp_servers]
41
42
  }.merge(additional_options)
42
43
  )
43
44
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ class Configuration
5
+ attr_accessor :verbose_generation_errors
6
+ attr_accessor :generation_retry_errors
7
+ attr_accessor :generation_max_retries
8
+ attr_accessor :generation_provider_logger
9
+
10
+ def initialize
11
+ @verbose_generation_errors = false
12
+ @generation_retry_errors = []
13
+ @generation_max_retries = 3
14
+ @generation_provider_logger = nil
15
+ end
16
+
17
+ def verbose_generation_errors?
18
+ @verbose_generation_errors
19
+ end
20
+ end
21
+
22
+ class << self
23
+ def configuration
24
+ @configuration ||= Configuration.new
25
+ end
26
+
27
+ def configure
28
+ yield configuration if block_given?
29
+ configuration
30
+ end
31
+
32
+ def reset_configuration!
33
+ @configuration = Configuration.new
34
+ end
35
+ end
36
+ end
@@ -10,23 +10,29 @@ end
10
10
  require "active_agent/action_prompt/action"
11
11
  require_relative "base"
12
12
  require_relative "response"
13
+ require_relative "stream_processing"
14
+ require_relative "message_formatting"
15
+ require_relative "tool_management"
13
16
 
14
17
  module ActiveAgent
15
18
  module GenerationProvider
16
19
  class AnthropicProvider < Base
20
+ include StreamProcessing
21
+ include MessageFormatting
22
+ include ToolManagement
17
23
  def initialize(config)
18
24
  super
19
25
  @access_token ||= config["api_key"] || config["access_token"] || Anthropic.configuration.access_token || ENV["ANTHROPIC_ACCESS_TOKEN"]
20
- @client = Anthropic::Client.new(access_token: @access_token)
26
+ @extra_headers = config["extra_headers"] || {}
27
+ @client = Anthropic::Client.new(access_token: @access_token, extra_headers: @extra_headers)
21
28
  end
22
29
 
23
30
  def generate(prompt)
24
31
  @prompt = prompt
25
32
 
26
- chat_prompt(parameters: prompt_parameters)
27
- rescue => e
28
- error_message = e.respond_to?(:message) ? e.message : e.to_s
29
- raise GenerationProviderError, error_message
33
+ with_error_handling do
34
+ chat_prompt(parameters: prompt_parameters)
35
+ end
30
36
  end
31
37
 
32
38
  def chat_prompt(parameters: prompt_parameters)
@@ -35,75 +41,75 @@ module ActiveAgent
35
41
  chat_response(@client.messages(parameters: parameters))
36
42
  end
37
43
 
38
- private
44
+ protected
39
45
 
40
- def provider_stream
41
- agent_stream = prompt.options[:stream]
42
- message = ActiveAgent::ActionPrompt::Message.new(content: "", role: :assistant)
43
- @response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt, message:)
46
+ # Override from StreamProcessing module for Anthropic-specific streaming
47
+ def process_stream_chunk(chunk, message, agent_stream)
48
+ if new_content = chunk.dig(:delta, :text)
49
+ message.content += new_content
50
+ agent_stream&.call(message, new_content, false, prompt.action_name)
51
+ end
44
52
 
45
- proc do |chunk|
46
- if new_content = chunk.dig(:delta, :text)
47
- message.content += new_content
48
- agent_stream.call(message, nil, false, prompt.action_name) if agent_stream.respond_to?(:call)
49
- end
53
+ if chunk[:type] == "message_stop"
54
+ finalize_stream(message, agent_stream)
50
55
  end
51
56
  end
52
57
 
53
- def prompt_parameters(model: @prompt.options[:model] || @model_name, messages: @prompt.messages, temperature: @prompt.options[:temperature] || @config["temperature"] || 0.7, tools: @prompt.actions)
54
- # fix for new Anthropic API that requires messages to be in a specific format without system role
55
- messages = messages.reject { |m| m.role == :system }
58
+ # Override from ParameterBuilder to handle Anthropic-specific requirements
59
+ def build_provider_parameters
60
+ # Anthropic requires system message separately and no system role in messages
61
+ filtered_messages = @prompt.messages.reject { |m| m.role == :system }
62
+ system_message = @prompt.messages.find { |m| m.role == :system }
63
+
56
64
  params = {
57
- model: model,
58
- system: @prompt.options[:instructions],
59
- messages: provider_messages(messages),
60
- temperature: temperature,
61
- max_tokens: @prompt.options[:max_tokens] || @config["max_tokens"] || 4096
65
+ system: system_message&.content || @prompt.options[:instructions]
62
66
  }
63
67
 
64
- if tools&.present?
65
- params[:tools] = format_tools(tools)
66
- end
68
+ # Override messages to use filtered version
69
+ @filtered_messages = filtered_messages
67
70
 
68
71
  params
69
72
  end
70
73
 
71
- def format_tools(tools)
72
- tools.map do |tool|
73
- {
74
- name: tool["name"] || tool["function"]["name"],
75
- description: tool["description"] || tool["function"]["description"],
76
- input_schema: tool["parameters"] || tool["function"]["parameters"]
77
- }
74
+ def build_base_parameters
75
+ super.tap do |params|
76
+ # Use filtered messages if available (set by build_provider_parameters)
77
+ params[:messages] = provider_messages(@filtered_messages || @prompt.messages)
78
+ # Anthropic requires max_tokens
79
+ params[:max_tokens] ||= 4096
78
80
  end
79
81
  end
80
82
 
81
- def provider_messages(messages)
82
- messages.map do |message|
83
- provider_message = {
84
- role: convert_role(message.role),
85
- content: []
86
- }
83
+ # Override from ToolManagement for Anthropic-specific tool format
84
+ def format_single_tool(tool)
85
+ {
86
+ name: tool["name"] || tool.dig("function", "name") || tool[:name] || tool.dig(:function, :name),
87
+ description: tool["description"] || tool.dig("function", "description") || tool[:description] || tool.dig(:function, :description),
88
+ input_schema: tool["parameters"] || tool.dig("function", "parameters") || tool[:parameters] || tool.dig(:function, :parameters)
89
+ }
90
+ end
87
91
 
88
- provider_message[:content] << if message.content_type == "image_url"
89
- {
90
- type: "image",
91
- source: {
92
- type: "url",
93
- url: message.content
94
- }
95
- }
96
- else
97
- {
98
- type: "text",
99
- text: message.content
100
- }
101
- end
102
-
103
- provider_message
92
+ # Override from MessageFormatting for Anthropic-specific message format
93
+ def format_content(message)
94
+ # Anthropic requires content as an array
95
+ if message.content_type == "image_url"
96
+ [ format_image_content(message).first ]
97
+ else
98
+ [ { type: "text", text: message.content } ]
104
99
  end
105
100
  end
106
101
 
102
+ def format_image_content(message)
103
+ [ {
104
+ type: "image",
105
+ source: {
106
+ type: "url",
107
+ url: message.content
108
+ }
109
+ } ]
110
+ end
111
+
112
+ # Override from MessageFormatting for Anthropic role mapping
107
113
  def convert_role(role)
108
114
  case role.to_s
109
115
  when "system" then "system"
@@ -123,7 +129,7 @@ module ActiveAgent
123
129
  content: content,
124
130
  role: "assistant",
125
131
  action_requested: response["stop_reason"] == "tool_use",
126
- requested_actions: handle_actions(response["tool_use"])
132
+ requested_actions: handle_actions(response["content"].map { |c| c if c["type"] == "tool_use" }.reject { |m| m.blank? }.to_a),
127
133
  )
128
134
 
129
135
  update_context(prompt: prompt, message: message, response: response)
@@ -135,17 +141,18 @@ module ActiveAgent
135
141
  )
136
142
  end
137
143
 
138
- def handle_actions(tool_uses)
139
- return unless tool_uses&.present?
144
+ # Override from ToolManagement for Anthropic-specific tool parsing
145
+ def parse_tool_call(tool_use)
146
+ return nil unless tool_use
140
147
 
141
- tool_uses.map do |tool_use|
142
- ActiveAgent::ActionPrompt::Action.new(
143
- id: tool_use[:id],
144
- name: tool_use[:name],
145
- params: tool_use[:input]
146
- )
147
- end
148
+ ActiveAgent::ActionPrompt::Action.new(
149
+ id: tool_use[:id],
150
+ name: tool_use[:name],
151
+ params: tool_use[:input]
152
+ )
148
153
  end
154
+
155
+ private
149
156
  end
150
157
  end
151
158
  end
@@ -1,8 +1,14 @@
1
1
  # lib/active_agent/generation_provider/base.rb
2
2
 
3
+ require_relative "error_handling"
4
+ require_relative "parameter_builder"
5
+
3
6
  module ActiveAgent
4
7
  module GenerationProvider
5
8
  class Base
9
+ include ErrorHandling
10
+ include ParameterBuilder
11
+
6
12
  class GenerationProviderError < StandardError; end
7
13
  attr_reader :client, :config, :prompt, :response, :access_token, :model_name
8
14
 
@@ -10,12 +16,18 @@ module ActiveAgent
10
16
  @config = config
11
17
  @prompt = nil
12
18
  @response = nil
19
+ @model_name = config["model"] if config
13
20
  end
14
21
 
15
22
  def generate(prompt)
16
23
  raise NotImplementedError, "Subclasses must implement the 'generate' method"
17
24
  end
18
25
 
26
+ def embed(prompt)
27
+ # Optional embedding support - override in providers that support it
28
+ raise NotImplementedError, "#{self.class.name} does not support embeddings"
29
+ end
30
+
19
31
  private
20
32
 
21
33
  def handle_response(response)
@@ -30,11 +42,12 @@ module ActiveAgent
30
42
 
31
43
  protected
32
44
 
33
- def prompt_parameters
34
- {
35
- messages: @prompt.messages,
36
- temperature: @config["temperature"] || 0.7
37
- }
45
+ # This method is now provided by ParameterBuilder module
46
+ # but can still be overridden if needed
47
+ def build_provider_parameters
48
+ # Base implementation returns empty hash
49
+ # Providers override this to add their specific parameters
50
+ {}
38
51
  end
39
52
  end
40
53
  end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module GenerationProvider
5
+ module ErrorHandling
6
+ extend ActiveSupport::Concern
7
+ include ActiveSupport::Rescuable
8
+
9
+ included do
10
+ class_attribute :retry_on_errors, default: []
11
+ class_attribute :max_retries, default: 3
12
+ class_attribute :verbose_errors_enabled, default: false
13
+
14
+ # Use rescue_from for provider-specific error handling
15
+ rescue_from StandardError, with: :handle_generation_error
16
+ end
17
+
18
+ def with_error_handling
19
+ retries = 0
20
+ begin
21
+ yield
22
+ rescue => e
23
+ if should_retry?(e) && retries < max_retries
24
+ retries += 1
25
+ log_retry(e, retries) if verbose_errors?
26
+ sleep(retry_delay(retries))
27
+ retry
28
+ else
29
+ # Use rescue_with_handler from Rescuable
30
+ rescue_with_handler(e) || raise
31
+ end
32
+ end
33
+ end
34
+
35
+ protected
36
+
37
+ def should_retry?(error)
38
+ return false if retry_on_errors.empty?
39
+ retry_on_errors.any? { |klass| error.is_a?(klass) }
40
+ end
41
+
42
+ def retry_delay(attempt)
43
+ # Exponential backoff: 1s, 2s, 4s...
44
+ 2 ** (attempt - 1)
45
+ end
46
+
47
+ def handle_generation_error(error)
48
+ error_message = format_error_message(error)
49
+
50
+ # Create new error with original backtrace preserved
51
+ new_error = ActiveAgent::GenerationProvider::Base::GenerationProviderError.new(error_message)
52
+ new_error.set_backtrace(error.backtrace) if error.respond_to?(:backtrace)
53
+
54
+ # Log detailed error if verbose mode is enabled
55
+ log_error_details(error) if verbose_errors?
56
+
57
+ # Instrument the error for LogSubscriber
58
+ instrument_error(error, new_error)
59
+
60
+ raise new_error
61
+ end
62
+
63
+ def format_error_message(error)
64
+ message = if error.respond_to?(:message)
65
+ error.message
66
+ elsif error.respond_to?(:to_s)
67
+ error.to_s
68
+ else
69
+ "An unknown error occurred: #{error.class.name}"
70
+ end
71
+
72
+ # Include error class in verbose mode
73
+ if verbose_errors?
74
+ "[#{error.class.name}] #{message}"
75
+ else
76
+ message
77
+ end
78
+ end
79
+
80
+ def verbose_errors?
81
+ # Check multiple sources for verbose setting (in priority order)
82
+ # 1. Instance config (highest priority)
83
+ return true if @config&.dig("verbose_errors")
84
+
85
+ # 2. Class-level setting
86
+ return true if self.class.verbose_errors_enabled
87
+
88
+ # 3. ActiveAgent global configuration
89
+ if defined?(ActiveAgent) && ActiveAgent.respond_to?(:configuration)
90
+ return true if ActiveAgent.configuration.verbose_generation_errors?
91
+ end
92
+
93
+ # 4. Environment variable (lowest priority)
94
+ ENV["ACTIVE_AGENT_VERBOSE_ERRORS"] == "true"
95
+ end
96
+
97
+ def log_error_details(error)
98
+ logger = find_logger
99
+ return unless logger
100
+
101
+ logger.error "[ActiveAgent::GenerationProvider] Error: #{error.class.name}: #{error.message}"
102
+ if logger.respond_to?(:debug) && error.respond_to?(:backtrace)
103
+ logger.debug "Backtrace:\n #{error.backtrace&.first(10)&.join("\n ")}"
104
+ end
105
+ end
106
+
107
+ def log_retry(error, attempt)
108
+ logger = find_logger
109
+ return unless logger
110
+
111
+ message = "[ActiveAgent::GenerationProvider] Retry attempt #{attempt}/#{max_retries} after #{error.class.name}"
112
+ logger.info message
113
+ end
114
+
115
+ def find_logger
116
+ # Try multiple logger sources (in priority order)
117
+ # 1. Instance config
118
+ return @config["logger"] if @config&.dig("logger")
119
+
120
+ # 2. ActiveAgent configuration logger
121
+ if defined?(ActiveAgent) && ActiveAgent.respond_to?(:configuration)
122
+ config_logger = ActiveAgent.configuration.generation_provider_logger
123
+ return config_logger if config_logger
124
+ end
125
+
126
+ # 3. Rails logger
127
+ return Rails.logger if defined?(Rails) && Rails.logger
128
+
129
+ # 4. ActiveAgent::Base logger
130
+ return ActiveAgent::Base.logger if defined?(ActiveAgent::Base) && ActiveAgent::Base.respond_to?(:logger)
131
+
132
+ nil
133
+ end
134
+
135
+ def instrument_error(original_error, wrapped_error)
136
+ if defined?(ActiveSupport::Notifications)
137
+ ActiveSupport::Notifications.instrument("error.active_agent", {
138
+ error_class: original_error.class.name,
139
+ error_message: original_error.message,
140
+ wrapped_error: wrapped_error,
141
+ provider: self.class.name
142
+ })
143
+ end
144
+ end
145
+
146
+ module ClassMethods
147
+ def retry_on(*errors, max_attempts: 3, **options)
148
+ self.retry_on_errors = errors
149
+ self.max_retries = max_attempts
150
+
151
+ # Also register with rescue_from for more complex handling
152
+ errors.each do |error_class|
153
+ rescue_from error_class do |error|
154
+ # This will be caught by with_error_handling for retry logic
155
+ raise error
156
+ end
157
+ end
158
+ end
159
+
160
+ def enable_verbose_errors!
161
+ self.verbose_errors_enabled = true
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end