a2a-ruby 1.0.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +137 -0
- data/.simplecov +46 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +33 -0
- data/CODE_OF_CONDUCT.md +128 -0
- data/CONTRIBUTING.md +165 -0
- data/Gemfile +43 -0
- data/Guardfile +34 -0
- data/LICENSE.txt +21 -0
- data/PUBLISHING_CHECKLIST.md +214 -0
- data/README.md +171 -0
- data/Rakefile +165 -0
- data/docs/agent_execution.md +309 -0
- data/docs/api_reference.md +792 -0
- data/docs/configuration.md +780 -0
- data/docs/events.md +475 -0
- data/docs/getting_started.md +668 -0
- data/docs/integration.md +262 -0
- data/docs/server_apps.md +621 -0
- data/docs/troubleshooting.md +765 -0
- data/lib/a2a/client/api_methods.rb +263 -0
- data/lib/a2a/client/auth/api_key.rb +161 -0
- data/lib/a2a/client/auth/interceptor.rb +288 -0
- data/lib/a2a/client/auth/jwt.rb +189 -0
- data/lib/a2a/client/auth/oauth2.rb +146 -0
- data/lib/a2a/client/auth.rb +137 -0
- data/lib/a2a/client/base.rb +316 -0
- data/lib/a2a/client/config.rb +210 -0
- data/lib/a2a/client/connection_pool.rb +233 -0
- data/lib/a2a/client/http_client.rb +524 -0
- data/lib/a2a/client/json_rpc_handler.rb +136 -0
- data/lib/a2a/client/middleware/circuit_breaker_interceptor.rb +245 -0
- data/lib/a2a/client/middleware/logging_interceptor.rb +371 -0
- data/lib/a2a/client/middleware/rate_limit_interceptor.rb +142 -0
- data/lib/a2a/client/middleware/retry_interceptor.rb +161 -0
- data/lib/a2a/client/middleware.rb +116 -0
- data/lib/a2a/client/performance_tracker.rb +60 -0
- data/lib/a2a/configuration/defaults.rb +34 -0
- data/lib/a2a/configuration/environment_loader.rb +76 -0
- data/lib/a2a/configuration/file_loader.rb +115 -0
- data/lib/a2a/configuration/inheritance.rb +101 -0
- data/lib/a2a/configuration/validator.rb +180 -0
- data/lib/a2a/configuration.rb +201 -0
- data/lib/a2a/errors.rb +291 -0
- data/lib/a2a/modules.rb +50 -0
- data/lib/a2a/monitoring/alerting.rb +490 -0
- data/lib/a2a/monitoring/distributed_tracing.rb +398 -0
- data/lib/a2a/monitoring/health_endpoints.rb +204 -0
- data/lib/a2a/monitoring/metrics_collector.rb +438 -0
- data/lib/a2a/monitoring.rb +463 -0
- data/lib/a2a/plugin.rb +358 -0
- data/lib/a2a/plugin_manager.rb +159 -0
- data/lib/a2a/plugins/example_auth.rb +81 -0
- data/lib/a2a/plugins/example_middleware.rb +118 -0
- data/lib/a2a/plugins/example_transport.rb +76 -0
- data/lib/a2a/protocol/agent_card.rb +8 -0
- data/lib/a2a/protocol/agent_card_server.rb +584 -0
- data/lib/a2a/protocol/capability.rb +496 -0
- data/lib/a2a/protocol/json_rpc.rb +254 -0
- data/lib/a2a/protocol/message.rb +8 -0
- data/lib/a2a/protocol/task.rb +8 -0
- data/lib/a2a/rails/a2a_controller.rb +258 -0
- data/lib/a2a/rails/controller_helpers.rb +499 -0
- data/lib/a2a/rails/engine.rb +167 -0
- data/lib/a2a/rails/generators/agent_generator.rb +311 -0
- data/lib/a2a/rails/generators/install_generator.rb +209 -0
- data/lib/a2a/rails/generators/migration_generator.rb +232 -0
- data/lib/a2a/rails/generators/templates/add_a2a_indexes.rb +57 -0
- data/lib/a2a/rails/generators/templates/agent_controller.rb +122 -0
- data/lib/a2a/rails/generators/templates/agent_controller_spec.rb +160 -0
- data/lib/a2a/rails/generators/templates/agent_readme.md +200 -0
- data/lib/a2a/rails/generators/templates/create_a2a_push_notification_configs.rb +68 -0
- data/lib/a2a/rails/generators/templates/create_a2a_tasks.rb +83 -0
- data/lib/a2a/rails/generators/templates/example_agent_controller.rb +228 -0
- data/lib/a2a/rails/generators/templates/initializer.rb +108 -0
- data/lib/a2a/rails/generators/templates/push_notification_config_model.rb +228 -0
- data/lib/a2a/rails/generators/templates/task_model.rb +200 -0
- data/lib/a2a/rails/tasks/a2a.rake +228 -0
- data/lib/a2a/server/a2a_methods.rb +520 -0
- data/lib/a2a/server/agent.rb +537 -0
- data/lib/a2a/server/agent_execution/agent_executor.rb +279 -0
- data/lib/a2a/server/agent_execution/request_context.rb +219 -0
- data/lib/a2a/server/apps/rack_app.rb +311 -0
- data/lib/a2a/server/apps/sinatra_app.rb +261 -0
- data/lib/a2a/server/default_request_handler.rb +350 -0
- data/lib/a2a/server/events/event_consumer.rb +116 -0
- data/lib/a2a/server/events/event_queue.rb +226 -0
- data/lib/a2a/server/example_agent.rb +248 -0
- data/lib/a2a/server/handler.rb +281 -0
- data/lib/a2a/server/middleware/authentication_middleware.rb +212 -0
- data/lib/a2a/server/middleware/cors_middleware.rb +171 -0
- data/lib/a2a/server/middleware/logging_middleware.rb +362 -0
- data/lib/a2a/server/middleware/rate_limit_middleware.rb +382 -0
- data/lib/a2a/server/middleware.rb +213 -0
- data/lib/a2a/server/push_notification_manager.rb +327 -0
- data/lib/a2a/server/request_handler.rb +136 -0
- data/lib/a2a/server/storage/base.rb +141 -0
- data/lib/a2a/server/storage/database.rb +266 -0
- data/lib/a2a/server/storage/memory.rb +274 -0
- data/lib/a2a/server/storage/redis.rb +320 -0
- data/lib/a2a/server/storage.rb +38 -0
- data/lib/a2a/server/task_manager.rb +534 -0
- data/lib/a2a/transport/grpc.rb +481 -0
- data/lib/a2a/transport/http.rb +415 -0
- data/lib/a2a/transport/sse.rb +499 -0
- data/lib/a2a/types/agent_card.rb +540 -0
- data/lib/a2a/types/artifact.rb +99 -0
- data/lib/a2a/types/base_model.rb +223 -0
- data/lib/a2a/types/events.rb +117 -0
- data/lib/a2a/types/message.rb +106 -0
- data/lib/a2a/types/part.rb +288 -0
- data/lib/a2a/types/push_notification.rb +139 -0
- data/lib/a2a/types/security.rb +167 -0
- data/lib/a2a/types/task.rb +154 -0
- data/lib/a2a/types.rb +88 -0
- data/lib/a2a/utils/helpers.rb +245 -0
- data/lib/a2a/utils/message_buffer.rb +278 -0
- data/lib/a2a/utils/performance.rb +247 -0
- data/lib/a2a/utils/rails_detection.rb +97 -0
- data/lib/a2a/utils/structured_logger.rb +306 -0
- data/lib/a2a/utils/time_helpers.rb +167 -0
- data/lib/a2a/utils/validation.rb +8 -0
- data/lib/a2a/version.rb +6 -0
- data/lib/a2a-rails.rb +58 -0
- data/lib/a2a.rb +198 -0
- metadata +437 -0
@@ -0,0 +1,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# Rate limiting interceptor using token bucket algorithm
|
5
|
+
#
|
6
|
+
# Implements client-side rate limiting to prevent overwhelming
|
7
|
+
# the target agent with too many requests.
|
8
|
+
#
|
9
|
+
module A2A
|
10
|
+
module Client
|
11
|
+
module Middleware
|
12
|
+
class RateLimitInterceptor
|
13
|
+
attr_reader :requests_per_second, :burst_size, :tokens, :last_refill
|
14
|
+
|
15
|
+
##
|
16
|
+
# Initialize rate limit interceptor
|
17
|
+
#
|
18
|
+
# @param requests_per_second [Numeric] Maximum requests per second (default: 10)
|
19
|
+
# @param burst_size [Integer] Maximum burst size (default: 20)
|
20
|
+
def initialize(requests_per_second: 10, burst_size: 20)
|
21
|
+
@requests_per_second = requests_per_second.to_f
|
22
|
+
@burst_size = burst_size
|
23
|
+
@tokens = @burst_size.to_f
|
24
|
+
@last_refill = Time.now
|
25
|
+
@mutex = Mutex.new
|
26
|
+
|
27
|
+
validate_configuration!
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Execute request with rate limiting
|
32
|
+
#
|
33
|
+
# @param request [Object] The request object
|
34
|
+
# @param context [Hash] Request context
|
35
|
+
# @param next_middleware [Proc] Next middleware in chain
|
36
|
+
# @return [Object] Response from next middleware
|
37
|
+
def call(request, context, next_middleware)
|
38
|
+
wait_for_token
|
39
|
+
next_middleware.call(request, context)
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Check if a request can be made immediately
|
44
|
+
#
|
45
|
+
# @return [Boolean] True if request can be made without waiting
|
46
|
+
def can_make_request?
|
47
|
+
@mutex.synchronize do
|
48
|
+
refill_tokens
|
49
|
+
@tokens >= 1.0
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# Get current rate limit status
|
55
|
+
#
|
56
|
+
# @return [Hash] Rate limit status information
|
57
|
+
def status
|
58
|
+
@mutex.synchronize do
|
59
|
+
refill_tokens
|
60
|
+
{
|
61
|
+
requests_per_second: @requests_per_second,
|
62
|
+
burst_size: @burst_size,
|
63
|
+
available_tokens: @tokens.round(2),
|
64
|
+
tokens_full: @tokens >= @burst_size,
|
65
|
+
can_make_request: @tokens >= 1.0
|
66
|
+
}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# Calculate time until next token is available
|
72
|
+
#
|
73
|
+
# @return [Float] Time in seconds until next token
|
74
|
+
def time_until_next_token
|
75
|
+
@mutex.synchronize do
|
76
|
+
refill_tokens
|
77
|
+
return 0.0 if @tokens >= 1.0
|
78
|
+
|
79
|
+
tokens_needed = 1.0 - @tokens
|
80
|
+
tokens_needed / @requests_per_second
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Reset the rate limiter (useful for testing)
|
86
|
+
def reset!
|
87
|
+
@mutex.synchronize do
|
88
|
+
@tokens = @burst_size.to_f
|
89
|
+
@last_refill = Time.now
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
##
|
96
|
+
# Wait for a token to become available
|
97
|
+
def wait_for_token
|
98
|
+
loop do
|
99
|
+
@mutex.synchronize do
|
100
|
+
refill_tokens
|
101
|
+
|
102
|
+
if @tokens >= 1.0
|
103
|
+
@tokens -= 1.0
|
104
|
+
return
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Calculate how long to sleep
|
109
|
+
sleep_time = time_until_next_token
|
110
|
+
sleep(sleep_time) if sleep_time.positive?
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Refill tokens based on elapsed time
|
116
|
+
def refill_tokens
|
117
|
+
now = Time.now
|
118
|
+
elapsed = now - @last_refill
|
119
|
+
|
120
|
+
return unless elapsed.positive?
|
121
|
+
|
122
|
+
# Add tokens based on elapsed time
|
123
|
+
tokens_to_add = elapsed * @requests_per_second
|
124
|
+
@tokens = [@tokens + tokens_to_add, @burst_size].min
|
125
|
+
@last_refill = now
|
126
|
+
end
|
127
|
+
|
128
|
+
##
|
129
|
+
# Validate configuration parameters
|
130
|
+
def validate_configuration!
|
131
|
+
raise ArgumentError, "requests_per_second must be positive" if @requests_per_second <= 0
|
132
|
+
|
133
|
+
raise ArgumentError, "burst_size must be positive" if @burst_size <= 0
|
134
|
+
|
135
|
+
return unless @burst_size < @requests_per_second
|
136
|
+
|
137
|
+
warn "Warning: burst_size (#{@burst_size}) is less than requests_per_second (#{@requests_per_second})"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# Retry interceptor with exponential backoff
|
5
|
+
#
|
6
|
+
# Automatically retries failed requests with configurable backoff
|
7
|
+
# strategy and error filtering.
|
8
|
+
#
|
9
|
+
module A2A
|
10
|
+
module Client
|
11
|
+
module Middleware
|
12
|
+
class RetryInterceptor
|
13
|
+
attr_reader :max_attempts, :initial_delay, :max_delay, :backoff_multiplier, :retryable_errors
|
14
|
+
|
15
|
+
# Default retryable error classes
|
16
|
+
DEFAULT_RETRYABLE_ERRORS = [
|
17
|
+
A2A::Errors::TimeoutError,
|
18
|
+
A2A::Errors::HTTPError,
|
19
|
+
A2A::Errors::TransportError,
|
20
|
+
A2A::Errors::AgentUnavailable,
|
21
|
+
A2A::Errors::ResourceExhausted,
|
22
|
+
Faraday::TimeoutError,
|
23
|
+
Faraday::ConnectionFailed
|
24
|
+
].freeze
|
25
|
+
|
26
|
+
##
|
27
|
+
# Initialize retry interceptor
|
28
|
+
#
|
29
|
+
# @param max_attempts [Integer] Maximum number of retry attempts (default: 3)
|
30
|
+
# @param initial_delay [Float] Initial delay in seconds (default: 1.0)
|
31
|
+
# @param max_delay [Float] Maximum delay in seconds (default: 60.0)
|
32
|
+
# @param backoff_multiplier [Float] Backoff multiplier (default: 2.0)
|
33
|
+
# @param retryable_errors [Array<Class>] List of retryable error classes
|
34
|
+
def initialize(max_attempts: 3, initial_delay: 1.0, max_delay: 60.0,
|
35
|
+
backoff_multiplier: 2.0, retryable_errors: nil)
|
36
|
+
@max_attempts = max_attempts
|
37
|
+
@initial_delay = initial_delay
|
38
|
+
@max_delay = max_delay
|
39
|
+
@backoff_multiplier = backoff_multiplier
|
40
|
+
@retryable_errors = retryable_errors || DEFAULT_RETRYABLE_ERRORS
|
41
|
+
|
42
|
+
validate_configuration!
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Execute request with retry logic
|
47
|
+
#
|
48
|
+
# @param request [Object] The request object
|
49
|
+
# @param context [Hash] Request context
|
50
|
+
# @param next_middleware [Proc] Next middleware in chain
|
51
|
+
# @return [Object] Response from successful request
|
52
|
+
def call(request, context, next_middleware)
|
53
|
+
attempt = 0
|
54
|
+
last_error = nil
|
55
|
+
|
56
|
+
loop do
|
57
|
+
attempt += 1
|
58
|
+
|
59
|
+
begin
|
60
|
+
return next_middleware.call(request, context)
|
61
|
+
rescue StandardError => e
|
62
|
+
last_error = e
|
63
|
+
|
64
|
+
# Check if we should retry
|
65
|
+
raise e unless should_retry?(e, attempt)
|
66
|
+
|
67
|
+
delay = calculate_delay(attempt)
|
68
|
+
context[:retry_attempt] = attempt
|
69
|
+
context[:retry_delay] = delay
|
70
|
+
|
71
|
+
sleep(delay) if delay.positive?
|
72
|
+
next
|
73
|
+
|
74
|
+
# Re-raise the error if we shouldn't retry or max attempts reached
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# Check if an error should trigger a retry
|
81
|
+
#
|
82
|
+
# @param error [Exception] The error that occurred
|
83
|
+
# @param attempt [Integer] Current attempt number
|
84
|
+
# @return [Boolean] True if should retry
|
85
|
+
def should_retry?(error, attempt)
|
86
|
+
return false if attempt >= @max_attempts
|
87
|
+
return false unless retryable_error?(error)
|
88
|
+
|
89
|
+
# Check for specific HTTP status codes that shouldn't be retried
|
90
|
+
if error.respond_to?(:status_code)
|
91
|
+
case error.status_code
|
92
|
+
when 400, 401, 403, 404, 422 # Client errors - don't retry
|
93
|
+
return false
|
94
|
+
when 429 # Rate limited - should retry
|
95
|
+
return true
|
96
|
+
when 500..599 # Server errors - should retry
|
97
|
+
return true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
true
|
102
|
+
end
|
103
|
+
|
104
|
+
##
|
105
|
+
# Check if an error is retryable
|
106
|
+
#
|
107
|
+
# @param error [Exception] The error to check
|
108
|
+
# @return [Boolean] True if error is retryable
|
109
|
+
def retryable_error?(error)
|
110
|
+
@retryable_errors.any? { |error_class| error.is_a?(error_class) }
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# Calculate delay for the given attempt
|
115
|
+
#
|
116
|
+
# @param attempt [Integer] Current attempt number (1-based)
|
117
|
+
# @return [Float] Delay in seconds
|
118
|
+
def calculate_delay(attempt)
|
119
|
+
return 0 if attempt <= 1
|
120
|
+
|
121
|
+
# Exponential backoff: initial_delay * (backoff_multiplier ^ (attempt - 2))
|
122
|
+
delay = @initial_delay * (@backoff_multiplier**(attempt - 2))
|
123
|
+
|
124
|
+
# Add jitter to prevent thundering herd
|
125
|
+
jitter = delay * 0.1 * rand
|
126
|
+
delay += jitter
|
127
|
+
|
128
|
+
# Cap at max_delay
|
129
|
+
[delay, @max_delay].min
|
130
|
+
end
|
131
|
+
|
132
|
+
##
|
133
|
+
# Get retry statistics
|
134
|
+
#
|
135
|
+
# @return [Hash] Retry configuration and statistics
|
136
|
+
def stats
|
137
|
+
{
|
138
|
+
max_attempts: @max_attempts,
|
139
|
+
initial_delay: @initial_delay,
|
140
|
+
max_delay: @max_delay,
|
141
|
+
backoff_multiplier: @backoff_multiplier,
|
142
|
+
retryable_errors: @retryable_errors.map(&:name)
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
##
|
149
|
+
# Validate configuration parameters
|
150
|
+
def validate_configuration!
|
151
|
+
raise ArgumentError, "max_attempts must be positive" if @max_attempts <= 0
|
152
|
+
raise ArgumentError, "initial_delay must be non-negative" if @initial_delay.negative?
|
153
|
+
raise ArgumentError, "max_delay must be positive" if @max_delay <= 0
|
154
|
+
raise ArgumentError, "backoff_multiplier must be positive" if @backoff_multiplier <= 0
|
155
|
+
|
156
|
+
raise ArgumentError, "initial_delay cannot be greater than max_delay" if @initial_delay > @max_delay
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "middleware/retry_interceptor"
|
4
|
+
require_relative "middleware/logging_interceptor"
|
5
|
+
require_relative "middleware/rate_limit_interceptor"
|
6
|
+
require_relative "middleware/circuit_breaker_interceptor"
|
7
|
+
|
8
|
+
##
|
9
|
+
# Client middleware system for A2A requests
|
10
|
+
#
|
11
|
+
# Provides a collection of middleware interceptors for handling
|
12
|
+
# cross-cutting concerns like retries, logging, rate limiting,
|
13
|
+
# and circuit breaking.
|
14
|
+
#
|
15
|
+
# @example Using middleware with a client
|
16
|
+
# retry_middleware = A2A::Client::Middleware::RetryInterceptor.new(max_attempts: 3)
|
17
|
+
# logging_middleware = A2A::Client::Middleware::LoggingInterceptor.new
|
18
|
+
#
|
19
|
+
# client = A2A::Client::HttpClient.new(
|
20
|
+
# 'https://agent.example.com',
|
21
|
+
# middleware: [retry_middleware, logging_middleware]
|
22
|
+
# )
|
23
|
+
#
|
24
|
+
module A2A
|
25
|
+
module Client
|
26
|
+
module Middleware
|
27
|
+
##
|
28
|
+
# Base class for middleware interceptors
|
29
|
+
#
|
30
|
+
class Base
|
31
|
+
##
|
32
|
+
# Call the middleware
|
33
|
+
#
|
34
|
+
# @param request [Object] The request object
|
35
|
+
# @param context [Hash] Request context
|
36
|
+
# @param next_middleware [Proc] Next middleware in chain
|
37
|
+
# @return [Object] Response from next middleware
|
38
|
+
def call(request, context, next_middleware)
|
39
|
+
raise NotImplementedError, "#{self.class}#call must be implemented"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Create middleware from configuration
|
45
|
+
#
|
46
|
+
# @param config [Hash] Middleware configuration
|
47
|
+
# @return [Base] Configured middleware instance
|
48
|
+
def self.from_config(config)
|
49
|
+
type = config["type"] || config[:type]
|
50
|
+
|
51
|
+
case type
|
52
|
+
when "retry"
|
53
|
+
RetryInterceptor.new(
|
54
|
+
max_attempts: config["max_attempts"] || config[:max_attempts] || 3,
|
55
|
+
initial_delay: config["initial_delay"] || config[:initial_delay] || 1.0,
|
56
|
+
max_delay: config["max_delay"] || config[:max_delay] || 60.0,
|
57
|
+
backoff_multiplier: config["backoff_multiplier"] || config[:backoff_multiplier] || 2.0,
|
58
|
+
retryable_errors: config["retryable_errors"] || config[:retryable_errors]
|
59
|
+
)
|
60
|
+
when "logging"
|
61
|
+
LoggingInterceptor.new(
|
62
|
+
logger: config["logger"] || config[:logger],
|
63
|
+
log_level: config["log_level"] || config[:log_level] || :info,
|
64
|
+
log_requests: config["log_requests"] || config[:log_requests] || true,
|
65
|
+
log_responses: config["log_responses"] || config[:log_responses] || true,
|
66
|
+
log_errors: config["log_errors"] || config[:log_errors] || true
|
67
|
+
)
|
68
|
+
when "rate_limit"
|
69
|
+
RateLimitInterceptor.new(
|
70
|
+
requests_per_second: config["requests_per_second"] || config[:requests_per_second] || 10,
|
71
|
+
burst_size: config["burst_size"] || config[:burst_size] || 20
|
72
|
+
)
|
73
|
+
when "circuit_breaker"
|
74
|
+
CircuitBreakerInterceptor.new(
|
75
|
+
failure_threshold: config["failure_threshold"] || config[:failure_threshold] || 5,
|
76
|
+
timeout: config["timeout"] || config[:timeout] || 60,
|
77
|
+
expected_errors: config["expected_errors"] || config[:expected_errors]
|
78
|
+
)
|
79
|
+
else
|
80
|
+
raise ArgumentError, "Unknown middleware type: #{type}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Create default middleware stack
|
86
|
+
#
|
87
|
+
# @param config [Hash] Configuration options
|
88
|
+
# @return [Array<Base>] Default middleware stack
|
89
|
+
def self.default_stack(config = {})
|
90
|
+
stack = []
|
91
|
+
|
92
|
+
# Add retry middleware
|
93
|
+
stack << RetryInterceptor.new if config.fetch("retry", true)
|
94
|
+
|
95
|
+
# Add logging middleware
|
96
|
+
stack << LoggingInterceptor.new if config.fetch("logging", true)
|
97
|
+
|
98
|
+
# Add rate limiting if configured
|
99
|
+
if config["rate_limit"]
|
100
|
+
stack << RateLimitInterceptor.new(
|
101
|
+
requests_per_second: config["rate_limit"]["requests_per_second"] || 10
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Add circuit breaker if configured
|
106
|
+
if config["circuit_breaker"]
|
107
|
+
stack << CircuitBreakerInterceptor.new(
|
108
|
+
failure_threshold: config["circuit_breaker"]["failure_threshold"] || 5
|
109
|
+
)
|
110
|
+
end
|
111
|
+
|
112
|
+
stack
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module A2A
|
4
|
+
module Client
|
5
|
+
##
|
6
|
+
# Performance tracking functionality for HTTP clients
|
7
|
+
#
|
8
|
+
module PerformanceTracker
|
9
|
+
##
|
10
|
+
# Initialize performance tracking
|
11
|
+
#
|
12
|
+
def initialize_performance_tracking
|
13
|
+
@performance_stats = {
|
14
|
+
requests_count: 0,
|
15
|
+
total_time: 0.0,
|
16
|
+
avg_response_time: 0.0,
|
17
|
+
cache_hits: 0,
|
18
|
+
cache_misses: 0
|
19
|
+
}
|
20
|
+
@stats_mutex = Mutex.new
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Get performance statistics
|
25
|
+
#
|
26
|
+
# @return [Hash] Performance statistics
|
27
|
+
def performance_stats
|
28
|
+
@stats_mutex.synchronize { @performance_stats.dup }
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Reset performance statistics
|
33
|
+
#
|
34
|
+
def reset_performance_stats!
|
35
|
+
@stats_mutex.synchronize do
|
36
|
+
@performance_stats = {
|
37
|
+
requests_count: 0,
|
38
|
+
total_time: 0.0,
|
39
|
+
avg_response_time: 0.0,
|
40
|
+
cache_hits: 0,
|
41
|
+
cache_misses: 0
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Record request performance metrics
|
48
|
+
#
|
49
|
+
# @param duration [Float] Request duration in seconds
|
50
|
+
def record_request_performance(duration)
|
51
|
+
@stats_mutex.synchronize do
|
52
|
+
@performance_stats[:requests_count] += 1
|
53
|
+
@performance_stats[:total_time] += duration
|
54
|
+
@performance_stats[:avg_response_time] =
|
55
|
+
@performance_stats[:total_time] / @performance_stats[:requests_count]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module A2A
|
4
|
+
class Configuration
|
5
|
+
# Module for handling default configuration values
|
6
|
+
module Defaults
|
7
|
+
private
|
8
|
+
|
9
|
+
# Load default configuration values
|
10
|
+
def load_defaults
|
11
|
+
@default_timeout = 30
|
12
|
+
@log_level = :info
|
13
|
+
@protocol_version = "0.3.0"
|
14
|
+
@default_transport = "JSONRPC"
|
15
|
+
@streaming_enabled = true
|
16
|
+
@push_notifications_enabled = true
|
17
|
+
@default_input_modes = ["text/plain", "application/json"]
|
18
|
+
@default_output_modes = ["text/plain", "application/json"]
|
19
|
+
@redis_config = { url: "redis://localhost:6379/0" }
|
20
|
+
@rails_integration = defined?(Rails) ? true : false
|
21
|
+
@mount_path = "/a2a"
|
22
|
+
@auto_mount = true
|
23
|
+
@middleware_enabled = true
|
24
|
+
@authentication_required = false
|
25
|
+
@cors_enabled = true
|
26
|
+
@rate_limiting_enabled = false
|
27
|
+
@logging_enabled = true
|
28
|
+
@webhook_authentication_required = false
|
29
|
+
@logger = nil
|
30
|
+
@user_agent = "A2A-Ruby/#{A2A::VERSION}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module A2A
|
4
|
+
class Configuration
|
5
|
+
# Module for loading configuration from environment variables
|
6
|
+
module EnvironmentLoader
|
7
|
+
private
|
8
|
+
|
9
|
+
# Load configuration from environment variables
|
10
|
+
def load_from_environment
|
11
|
+
@default_timeout = env_int("A2A_DEFAULT_TIMEOUT", @default_timeout)
|
12
|
+
@log_level = env_symbol("A2A_LOG_LEVEL", @log_level)
|
13
|
+
@protocol_version = env_string("A2A_PROTOCOL_VERSION", @protocol_version)
|
14
|
+
@default_transport = env_string("A2A_DEFAULT_TRANSPORT", @default_transport)
|
15
|
+
@streaming_enabled = env_bool("A2A_STREAMING_ENABLED", @streaming_enabled)
|
16
|
+
@push_notifications_enabled = env_bool("A2A_PUSH_NOTIFICATIONS_ENABLED", @push_notifications_enabled)
|
17
|
+
@default_input_modes = env_array("A2A_DEFAULT_INPUT_MODES", @default_input_modes)
|
18
|
+
@default_output_modes = env_array("A2A_DEFAULT_OUTPUT_MODES", @default_output_modes)
|
19
|
+
@rails_integration = env_bool("A2A_RAILS_INTEGRATION", @rails_integration)
|
20
|
+
@mount_path = env_string("A2A_MOUNT_PATH", @mount_path)
|
21
|
+
@auto_mount = env_bool("A2A_AUTO_MOUNT", @auto_mount)
|
22
|
+
@middleware_enabled = env_bool("A2A_MIDDLEWARE_ENABLED", @middleware_enabled)
|
23
|
+
@authentication_required = env_bool("A2A_AUTHENTICATION_REQUIRED", @authentication_required)
|
24
|
+
@cors_enabled = env_bool("A2A_CORS_ENABLED", @cors_enabled)
|
25
|
+
@rate_limiting_enabled = env_bool("A2A_RATE_LIMITING_ENABLED", @rate_limiting_enabled)
|
26
|
+
@logging_enabled = env_bool("A2A_LOGGING_ENABLED", @logging_enabled)
|
27
|
+
@webhook_authentication_required = env_bool("A2A_WEBHOOK_AUTHENTICATION_REQUIRED",
|
28
|
+
@webhook_authentication_required)
|
29
|
+
@user_agent = env_string("A2A_USER_AGENT", @user_agent)
|
30
|
+
|
31
|
+
# Redis configuration from environment
|
32
|
+
redis_url = ENV["REDIS_URL"] || ENV.fetch("A2A_REDIS_URL", nil)
|
33
|
+
return unless redis_url
|
34
|
+
|
35
|
+
@redis_config = { url: redis_url }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Detect current environment
|
39
|
+
def detect_environment
|
40
|
+
return ENV["A2A_ENV"] if ENV["A2A_ENV"]
|
41
|
+
return ENV["RAILS_ENV"] if ENV["RAILS_ENV"]
|
42
|
+
return ENV["RACK_ENV"] if ENV["RACK_ENV"]
|
43
|
+
return rails_environment if rails_environment
|
44
|
+
|
45
|
+
"development"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Environment variable helpers
|
49
|
+
def env_string(key, default)
|
50
|
+
ENV[key] || default
|
51
|
+
end
|
52
|
+
|
53
|
+
def env_int(key, default)
|
54
|
+
value = ENV.fetch(key, nil)
|
55
|
+
value ? value.to_i : default
|
56
|
+
end
|
57
|
+
|
58
|
+
def env_bool(key, default)
|
59
|
+
value = ENV.fetch(key, nil)
|
60
|
+
return default if value.nil?
|
61
|
+
|
62
|
+
%w[true yes 1 on].include?(value.downcase)
|
63
|
+
end
|
64
|
+
|
65
|
+
def env_symbol(key, default)
|
66
|
+
value = ENV.fetch(key, nil)
|
67
|
+
value ? value.to_sym : default
|
68
|
+
end
|
69
|
+
|
70
|
+
def env_array(key, default)
|
71
|
+
value = ENV.fetch(key, nil)
|
72
|
+
value ? value.split(",").map(&:strip) : default
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|