activeagent 0.5.0 → 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 +4 -4
- data/lib/active_agent/action_prompt/base.rb +31 -13
- data/lib/active_agent/action_prompt/prompt.rb +4 -2
- data/lib/active_agent/base.rb +2 -1
- data/lib/active_agent/configuration.rb +36 -0
- data/lib/active_agent/generation_provider/anthropic_provider.rb +72 -65
- data/lib/active_agent/generation_provider/base.rb +18 -5
- data/lib/active_agent/generation_provider/error_handling.rb +166 -0
- data/lib/active_agent/generation_provider/log_subscriber.rb +92 -0
- data/lib/active_agent/generation_provider/message_formatting.rb +107 -0
- data/lib/active_agent/generation_provider/open_ai_provider.rb +47 -80
- data/lib/active_agent/generation_provider/open_router_provider.rb +330 -2
- data/lib/active_agent/generation_provider/parameter_builder.rb +119 -0
- data/lib/active_agent/generation_provider/response.rb +3 -1
- data/lib/active_agent/generation_provider/stream_processing.rb +58 -0
- data/lib/active_agent/generation_provider/tool_management.rb +142 -0
- data/lib/active_agent/generation_provider.rb +1 -1
- data/lib/active_agent/log_subscriber.rb +6 -6
- data/lib/active_agent/parameterized.rb +1 -0
- data/lib/active_agent/sanitizers.rb +40 -0
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +9 -6
- data/lib/generators/erb/agent_generator.rb +3 -0
- data/lib/generators/erb/templates/instructions.text.erb.tt +1 -0
- metadata +38 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 01b5ef5633bece3cdce9d9dba58e499b48c57d80c3a461ceda58adbff738e36a
|
4
|
+
data.tar.gz: 9cc9065b10e9bbf0f8fdeba420fe7b9518b7e97f2d32e3bf11af55babd697009
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 =
|
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
|
-
|
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
|
-
#
|
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 =
|
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
|
-
|
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
|
392
|
-
|
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:
|
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
|
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" +
|
data/lib/active_agent/base.rb
CHANGED
@@ -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
|
-
@
|
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
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
44
|
+
protected
|
39
45
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
46
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
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
|
-
|
65
|
-
|
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
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
139
|
-
|
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
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|