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,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "auth/oauth2"
|
4
|
+
require_relative "auth/jwt"
|
5
|
+
require_relative "auth/api_key"
|
6
|
+
require_relative "auth/interceptor"
|
7
|
+
|
8
|
+
##
|
9
|
+
# Authentication strategies and utilities for A2A clients
|
10
|
+
#
|
11
|
+
# This module provides various authentication mechanisms for communicating
|
12
|
+
# with A2A agents, including OAuth 2.0, JWT, and API key authentication.
|
13
|
+
#
|
14
|
+
# @example OAuth 2.0 authentication
|
15
|
+
# oauth = A2A::Client::Auth::OAuth2.new(
|
16
|
+
# client_id: 'your-client-id',
|
17
|
+
# client_secret: 'your-client-secret',
|
18
|
+
# token_url: 'https://auth.example.com/oauth/token'
|
19
|
+
# )
|
20
|
+
#
|
21
|
+
# client = A2A::Client::HttpClient.new(
|
22
|
+
# 'https://agent.example.com',
|
23
|
+
# middleware: [A2A::Client::Auth::Interceptor.new(oauth)]
|
24
|
+
# )
|
25
|
+
#
|
26
|
+
# @example JWT authentication
|
27
|
+
# jwt = A2A::Client::Auth::JWT.new(
|
28
|
+
# token: 'your-jwt-token'
|
29
|
+
# )
|
30
|
+
#
|
31
|
+
# interceptor = A2A::Client::Auth::Interceptor.new(jwt)
|
32
|
+
#
|
33
|
+
# @example API key authentication
|
34
|
+
# api_key = A2A::Client::Auth::ApiKey.new(
|
35
|
+
# key: 'your-api-key',
|
36
|
+
# name: 'X-API-Key',
|
37
|
+
# location: 'header'
|
38
|
+
# )
|
39
|
+
#
|
40
|
+
# interceptor = A2A::Client::Auth::Interceptor.new(api_key)
|
41
|
+
#
|
42
|
+
module A2A
|
43
|
+
module Client
|
44
|
+
module Auth
|
45
|
+
##
|
46
|
+
# Create authentication strategy from configuration
|
47
|
+
#
|
48
|
+
# @param config [Hash] Authentication configuration
|
49
|
+
# @return [Object] Authentication strategy instance
|
50
|
+
def self.from_config(config)
|
51
|
+
case config["type"] || config[:type]
|
52
|
+
when "oauth2"
|
53
|
+
OAuth2.new(
|
54
|
+
client_id: config["client_id"] || config[:client_id],
|
55
|
+
client_secret: config["client_secret"] || config[:client_secret],
|
56
|
+
token_url: config["token_url"] || config[:token_url],
|
57
|
+
scope: config["scope"] || config[:scope]
|
58
|
+
)
|
59
|
+
when "jwt"
|
60
|
+
JWT.new(
|
61
|
+
token: config["token"] || config[:token],
|
62
|
+
secret: config["secret"] || config[:secret],
|
63
|
+
algorithm: config["algorithm"] || config[:algorithm] || "HS256",
|
64
|
+
payload: config["payload"] || config[:payload],
|
65
|
+
headers: config["headers"] || config[:headers],
|
66
|
+
expires_in: config["expires_in"] || config[:expires_in]
|
67
|
+
)
|
68
|
+
when "api_key"
|
69
|
+
ApiKey.new(
|
70
|
+
key: config["key"] || config[:key],
|
71
|
+
name: config["name"] || config[:name] || "X-API-Key",
|
72
|
+
location: config["location"] || config[:location] || "header"
|
73
|
+
)
|
74
|
+
else
|
75
|
+
raise ArgumentError, "Unknown authentication type: #{config['type'] || config[:type]}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# Create authentication strategy from security scheme
|
81
|
+
#
|
82
|
+
# @param scheme [Hash] Security scheme definition from agent card
|
83
|
+
# @param credentials [Hash] Authentication credentials
|
84
|
+
# @return [Object] Authentication strategy instance
|
85
|
+
def self.from_security_scheme(scheme, credentials)
|
86
|
+
case scheme["type"]
|
87
|
+
when "oauth2"
|
88
|
+
OAuth2.new(
|
89
|
+
client_id: credentials["client_id"],
|
90
|
+
client_secret: credentials["client_secret"],
|
91
|
+
token_url: scheme["tokenUrl"],
|
92
|
+
scope: credentials["scope"]
|
93
|
+
)
|
94
|
+
when "http"
|
95
|
+
case scheme["scheme"]
|
96
|
+
when "bearer"
|
97
|
+
JWT.new(token: credentials["token"])
|
98
|
+
when "basic"
|
99
|
+
# Basic auth configuration
|
100
|
+
{
|
101
|
+
type: "basic",
|
102
|
+
username: credentials["username"],
|
103
|
+
password: credentials["password"]
|
104
|
+
}
|
105
|
+
else
|
106
|
+
raise ArgumentError, "Unsupported HTTP scheme: #{scheme['scheme']}"
|
107
|
+
end
|
108
|
+
when "apiKey"
|
109
|
+
ApiKey.from_security_scheme(scheme, credentials["key"])
|
110
|
+
else
|
111
|
+
raise ArgumentError, "Unsupported security scheme type: #{scheme['type']}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Create interceptor from configuration
|
117
|
+
#
|
118
|
+
# @param config [Hash] Authentication configuration
|
119
|
+
# @return [Interceptor] Configured authentication interceptor
|
120
|
+
def self.interceptor_from_config(config)
|
121
|
+
strategy = from_config(config)
|
122
|
+
Interceptor.new(strategy, auto_retry: config["auto_retry"] || config[:auto_retry] || true)
|
123
|
+
end
|
124
|
+
|
125
|
+
##
|
126
|
+
# Create interceptor from security scheme
|
127
|
+
#
|
128
|
+
# @param scheme [Hash] Security scheme definition
|
129
|
+
# @param credentials [Hash] Authentication credentials
|
130
|
+
# @return [Interceptor] Configured authentication interceptor
|
131
|
+
def self.interceptor_from_security_scheme(scheme, credentials)
|
132
|
+
strategy = from_security_scheme(scheme, credentials)
|
133
|
+
Interceptor.new(strategy)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,316 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "config"
|
4
|
+
|
5
|
+
##
|
6
|
+
# Abstract base class for A2A clients
|
7
|
+
#
|
8
|
+
# Provides the common interface and functionality for all A2A client implementations.
|
9
|
+
# Concrete clients should inherit from this class and implement the abstract methods.
|
10
|
+
#
|
11
|
+
module A2A
|
12
|
+
module Client
|
13
|
+
class Base
|
14
|
+
attr_reader :config, :middleware, :consumers
|
15
|
+
|
16
|
+
##
|
17
|
+
# Initialize a new client
|
18
|
+
#
|
19
|
+
# @param config [Config, nil] Client configuration
|
20
|
+
# @param middleware [Array] List of middleware interceptors
|
21
|
+
# @param consumers [Array] List of event consumers
|
22
|
+
def initialize(config: nil, middleware: [], consumers: [])
|
23
|
+
@config = config || Config.new
|
24
|
+
@middleware = middleware.dup
|
25
|
+
@consumers = consumers.dup
|
26
|
+
@task_callbacks = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Send a message to the agent
|
31
|
+
#
|
32
|
+
# @param message [Message, Hash] The message to send
|
33
|
+
# @param context [Hash, nil] Optional context information
|
34
|
+
# @return [Enumerator, Message] Stream of responses or single response
|
35
|
+
# @raise [NotImplementedError] Must be implemented by subclasses
|
36
|
+
def send_message(message, context: nil)
|
37
|
+
raise NotImplementedError, "#{self.class}#send_message must be implemented"
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Get a task by ID
|
42
|
+
#
|
43
|
+
# @param task_id [String] The task ID
|
44
|
+
# @param context [Hash, nil] Optional context information
|
45
|
+
# @param history_length [Integer, nil] Maximum number of history messages to include
|
46
|
+
# @return [Task] The task
|
47
|
+
# @raise [NotImplementedError] Must be implemented by subclasses
|
48
|
+
def get_task(task_id, context: nil, history_length: nil)
|
49
|
+
raise NotImplementedError, "#{self.class}#get_task must be implemented"
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Cancel a task
|
54
|
+
#
|
55
|
+
# @param task_id [String] The task ID to cancel
|
56
|
+
# @param context [Hash, nil] Optional context information
|
57
|
+
# @return [Task] The updated task
|
58
|
+
# @raise [NotImplementedError] Must be implemented by subclasses
|
59
|
+
def cancel_task(task_id, context: nil)
|
60
|
+
raise NotImplementedError, "#{self.class}#cancel_task must be implemented"
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Get the agent card
|
65
|
+
#
|
66
|
+
# @param context [Hash, nil] Optional context information
|
67
|
+
# @param authenticated [Boolean] Whether to get authenticated extended card
|
68
|
+
# @return [AgentCard] The agent card
|
69
|
+
# @raise [NotImplementedError] Must be implemented by subclasses
|
70
|
+
def get_card(context: nil, authenticated: false)
|
71
|
+
raise NotImplementedError, "#{self.class}#get_card must be implemented"
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Resubscribe to a task for streaming updates
|
76
|
+
#
|
77
|
+
# @param task_id [String] The task ID to resubscribe to
|
78
|
+
# @param context [Hash, nil] Optional context information
|
79
|
+
# @return [Enumerator] Stream of task updates
|
80
|
+
# @raise [NotImplementedError] Must be implemented by subclasses
|
81
|
+
def resubscribe(task_id, context: nil)
|
82
|
+
raise NotImplementedError, "#{self.class}#resubscribe must be implemented"
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# Set a callback for task updates
|
87
|
+
#
|
88
|
+
# @param task_id [String] The task ID
|
89
|
+
# @param push_notification_config [PushNotificationConfig, Hash] The push notification configuration
|
90
|
+
# @param context [Hash, nil] Optional context information
|
91
|
+
# @return [void]
|
92
|
+
# @raise [NotImplementedError] Must be implemented by subclasses
|
93
|
+
def set_task_callback(task_id, push_notification_config, context: nil)
|
94
|
+
raise NotImplementedError, "#{self.class}#set_task_callback must be implemented"
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# Get the callback configuration for a task
|
99
|
+
#
|
100
|
+
# @param task_id [String] The task ID
|
101
|
+
# @param push_notification_config_id [String] The push notification config ID
|
102
|
+
# @param context [Hash, nil] Optional context information
|
103
|
+
# @return [TaskPushNotificationConfig] The callback configuration
|
104
|
+
# @raise [NotImplementedError] Must be implemented by subclasses
|
105
|
+
def get_task_callback(task_id, push_notification_config_id, context: nil)
|
106
|
+
raise NotImplementedError, "#{self.class}#get_task_callback must be implemented"
|
107
|
+
end
|
108
|
+
|
109
|
+
##
|
110
|
+
# List all callback configurations for a task
|
111
|
+
#
|
112
|
+
# @param task_id [String] The task ID
|
113
|
+
# @param context [Hash, nil] Optional context information
|
114
|
+
# @return [Array<TaskPushNotificationConfig>] List of callback configurations
|
115
|
+
# @raise [NotImplementedError] Must be implemented by subclasses
|
116
|
+
def list_task_callbacks(task_id, context: nil)
|
117
|
+
raise NotImplementedError, "#{self.class}#list_task_callbacks must be implemented"
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# Delete a callback configuration for a task
|
122
|
+
#
|
123
|
+
# @param task_id [String] The task ID
|
124
|
+
# @param push_notification_config_id [String] The push notification config ID
|
125
|
+
# @param context [Hash, nil] Optional context information
|
126
|
+
# @return [void]
|
127
|
+
# @raise [NotImplementedError] Must be implemented by subclasses
|
128
|
+
def delete_task_callback(task_id, push_notification_config_id, context: nil)
|
129
|
+
raise NotImplementedError, "#{self.class}#delete_task_callback must be implemented"
|
130
|
+
end
|
131
|
+
|
132
|
+
##
|
133
|
+
# Add middleware to the client
|
134
|
+
#
|
135
|
+
# @param interceptor [Object] The middleware interceptor
|
136
|
+
# @return [void]
|
137
|
+
def add_middleware(interceptor)
|
138
|
+
@middleware << interceptor
|
139
|
+
end
|
140
|
+
|
141
|
+
##
|
142
|
+
# Remove middleware from the client
|
143
|
+
#
|
144
|
+
# @param interceptor [Object] The middleware interceptor to remove
|
145
|
+
# @return [void]
|
146
|
+
def remove_middleware(interceptor)
|
147
|
+
@middleware.delete(interceptor)
|
148
|
+
end
|
149
|
+
|
150
|
+
##
|
151
|
+
# Add an event consumer
|
152
|
+
#
|
153
|
+
# @param consumer [Object] The event consumer
|
154
|
+
# @return [void]
|
155
|
+
def add_consumer(consumer)
|
156
|
+
@consumers << consumer
|
157
|
+
end
|
158
|
+
|
159
|
+
##
|
160
|
+
# Remove an event consumer
|
161
|
+
#
|
162
|
+
# @param consumer [Object] The event consumer to remove
|
163
|
+
# @return [void]
|
164
|
+
def remove_consumer(consumer)
|
165
|
+
@consumers.delete(consumer)
|
166
|
+
end
|
167
|
+
|
168
|
+
##
|
169
|
+
# Check if the client supports streaming
|
170
|
+
#
|
171
|
+
# @return [Boolean] True if streaming is supported and enabled
|
172
|
+
def streaming?
|
173
|
+
@config.streaming?
|
174
|
+
end
|
175
|
+
|
176
|
+
##
|
177
|
+
# Check if the client supports polling
|
178
|
+
#
|
179
|
+
# @return [Boolean] True if polling is supported and enabled
|
180
|
+
def polling?
|
181
|
+
@config.polling?
|
182
|
+
end
|
183
|
+
|
184
|
+
##
|
185
|
+
# Get the supported transports
|
186
|
+
#
|
187
|
+
# @return [Array<String>] List of supported transport protocols
|
188
|
+
def supported_transports
|
189
|
+
@config.supported_transports
|
190
|
+
end
|
191
|
+
|
192
|
+
##
|
193
|
+
# Negotiate transport with agent card
|
194
|
+
#
|
195
|
+
# @param agent_card [AgentCard] The agent card
|
196
|
+
# @return [String] The negotiated transport protocol
|
197
|
+
def negotiate_transport(agent_card)
|
198
|
+
# Use client preference if enabled
|
199
|
+
if @config.use_client_preference?
|
200
|
+
preferred = @config.preferred_transport
|
201
|
+
return preferred if agent_supports_transport?(agent_card, preferred)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Find first mutually supported transport
|
205
|
+
@config.supported_transports.each do |transport|
|
206
|
+
return transport if agent_supports_transport?(agent_card, transport)
|
207
|
+
end
|
208
|
+
|
209
|
+
# Fallback to agent's preferred transport if we support it
|
210
|
+
agent_preferred = agent_card.preferred_transport
|
211
|
+
return agent_preferred if @config.supports_transport?(agent_preferred)
|
212
|
+
|
213
|
+
# No compatible transport found
|
214
|
+
raise A2A::Errors::ClientError, "No compatible transport protocol found"
|
215
|
+
end
|
216
|
+
|
217
|
+
##
|
218
|
+
# Get the endpoint URL for a specific transport
|
219
|
+
#
|
220
|
+
# @param agent_card [AgentCard] The agent card
|
221
|
+
# @param transport [String] The transport protocol
|
222
|
+
# @return [String] The endpoint URL
|
223
|
+
def get_endpoint_url(agent_card, transport)
|
224
|
+
# Check if the transport matches the preferred transport
|
225
|
+
return agent_card.url if agent_card.preferred_transport == transport
|
226
|
+
|
227
|
+
# Look for the transport in additional interfaces
|
228
|
+
interface = agent_card.additional_interfaces&.find { |iface| iface.transport == transport }
|
229
|
+
return interface.url if interface
|
230
|
+
|
231
|
+
# Fallback to main URL if no specific interface found
|
232
|
+
agent_card.url
|
233
|
+
end
|
234
|
+
|
235
|
+
protected
|
236
|
+
|
237
|
+
##
|
238
|
+
# Execute middleware chain for a request
|
239
|
+
#
|
240
|
+
# @param request [Object] The request object
|
241
|
+
# @param context [Hash] The request context
|
242
|
+
# @yield [request, context] The block to execute after middleware
|
243
|
+
# @return [Object] The result of the block execution
|
244
|
+
def execute_with_middleware(request, context = {}, &block)
|
245
|
+
# Create a chain of middleware calls
|
246
|
+
chain = @middleware.reverse.reduce(proc(&block)) do |next_call, middleware|
|
247
|
+
proc { |req, ctx| middleware.call(req, ctx, next_call) }
|
248
|
+
end
|
249
|
+
|
250
|
+
# Execute the chain
|
251
|
+
chain.call(request, context)
|
252
|
+
end
|
253
|
+
|
254
|
+
##
|
255
|
+
# Process events with registered consumers
|
256
|
+
#
|
257
|
+
# @param event [Object] The event to process
|
258
|
+
# @return [void]
|
259
|
+
def process_event(event)
|
260
|
+
@consumers.each do |consumer|
|
261
|
+
consumer.call(event)
|
262
|
+
rescue StandardError => e
|
263
|
+
# Log error but don't fail the entire processing
|
264
|
+
warn "Error in event consumer: #{e.message}"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
##
|
269
|
+
# Convert a message hash or object to a Message instance
|
270
|
+
#
|
271
|
+
# @param message [Message, Hash] The message to convert
|
272
|
+
# @return [Message] The message instance
|
273
|
+
def ensure_message(message)
|
274
|
+
return message if message.is_a?(A2A::Types::Message)
|
275
|
+
|
276
|
+
A2A::Types::Message.from_h(message)
|
277
|
+
end
|
278
|
+
|
279
|
+
##
|
280
|
+
# Convert a task hash or object to a Task instance
|
281
|
+
#
|
282
|
+
# @param task [Task, Hash] The task to convert
|
283
|
+
# @return [Task] The task instance
|
284
|
+
def ensure_task(task)
|
285
|
+
return task if task.is_a?(A2A::Types::Task)
|
286
|
+
|
287
|
+
A2A::Types::Task.from_h(task)
|
288
|
+
end
|
289
|
+
|
290
|
+
##
|
291
|
+
# Convert an agent card hash or object to an AgentCard instance
|
292
|
+
#
|
293
|
+
# @param agent_card [AgentCard, Hash] The agent card to convert
|
294
|
+
# @return [AgentCard] The agent card instance
|
295
|
+
def ensure_agent_card(agent_card)
|
296
|
+
return agent_card if agent_card.is_a?(A2A::Types::AgentCard)
|
297
|
+
|
298
|
+
A2A::Types::AgentCard.from_h(agent_card)
|
299
|
+
end
|
300
|
+
|
301
|
+
private
|
302
|
+
|
303
|
+
##
|
304
|
+
# Check if an agent supports a specific transport
|
305
|
+
#
|
306
|
+
# @param agent_card [AgentCard] The agent card
|
307
|
+
# @param transport [String] The transport to check
|
308
|
+
# @return [Boolean] True if the agent supports the transport
|
309
|
+
def agent_supports_transport?(agent_card, transport)
|
310
|
+
return true if agent_card.preferred_transport == transport
|
311
|
+
|
312
|
+
agent_card.additional_interfaces&.any? { |iface| iface.transport == transport }
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../version"
|
4
|
+
|
5
|
+
##
|
6
|
+
# Configuration class for A2A clients
|
7
|
+
#
|
8
|
+
# Manages client behavior including transport preferences, streaming options,
|
9
|
+
# authentication settings, and operational parameters.
|
10
|
+
#
|
11
|
+
module A2A
|
12
|
+
module Client
|
13
|
+
class Config
|
14
|
+
attr_accessor :streaming, :polling, :supported_transports, :use_client_preference,
|
15
|
+
:accepted_output_modes, :push_notification_configs, :timeout,
|
16
|
+
:retry_attempts, :retry_delay, :max_retry_delay, :backoff_multiplier,
|
17
|
+
:endpoint_url, :authentication, :headers, :user_agent
|
18
|
+
|
19
|
+
##
|
20
|
+
# Initialize a new client configuration
|
21
|
+
#
|
22
|
+
# @param streaming [Boolean] Enable streaming responses (default: true)
|
23
|
+
# @param polling [Boolean] Enable polling for task updates (default: false)
|
24
|
+
# @param supported_transports [Array<String>] Supported transport protocols
|
25
|
+
# @param use_client_preference [Boolean] Use client transport preference (default: true)
|
26
|
+
# @param accepted_output_modes [Array<String>] Accepted output modes
|
27
|
+
# @param push_notification_configs [Array<Hash>] Push notification configurations
|
28
|
+
# @param timeout [Integer] Request timeout in seconds (default: 30)
|
29
|
+
# @param retry_attempts [Integer] Number of retry attempts (default: 3)
|
30
|
+
# @param retry_delay [Float] Initial retry delay in seconds (default: 1.0)
|
31
|
+
# @param max_retry_delay [Float] Maximum retry delay in seconds (default: 60.0)
|
32
|
+
# @param backoff_multiplier [Float] Backoff multiplier for retries (default: 2.0)
|
33
|
+
# @param endpoint_url [String] Base endpoint URL
|
34
|
+
# @param authentication [Hash] Authentication configuration
|
35
|
+
# @param headers [Hash] Additional HTTP headers
|
36
|
+
# @param user_agent [String] User agent string
|
37
|
+
def initialize(streaming: true, polling: false, supported_transports: nil,
|
38
|
+
use_client_preference: true, accepted_output_modes: nil,
|
39
|
+
push_notification_configs: nil, timeout: 30, retry_attempts: 3,
|
40
|
+
retry_delay: 1.0, max_retry_delay: 60.0, backoff_multiplier: 2.0,
|
41
|
+
endpoint_url: nil, authentication: nil, headers: nil, user_agent: nil)
|
42
|
+
@streaming = streaming
|
43
|
+
@polling = polling
|
44
|
+
@supported_transports = supported_transports || [A2A::Types::TRANSPORT_JSONRPC]
|
45
|
+
@use_client_preference = use_client_preference
|
46
|
+
@accepted_output_modes = accepted_output_modes || %w[text file data]
|
47
|
+
@push_notification_configs = push_notification_configs || []
|
48
|
+
@timeout = timeout
|
49
|
+
@retry_attempts = retry_attempts
|
50
|
+
@retry_delay = retry_delay
|
51
|
+
@max_retry_delay = max_retry_delay
|
52
|
+
@backoff_multiplier = backoff_multiplier
|
53
|
+
@endpoint_url = endpoint_url
|
54
|
+
@authentication = authentication || {}
|
55
|
+
@headers = headers || {}
|
56
|
+
@user_agent = user_agent || "a2a-ruby/#{A2A::VERSION}"
|
57
|
+
|
58
|
+
validate!
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Check if streaming is enabled
|
63
|
+
#
|
64
|
+
# @return [Boolean] True if streaming is enabled
|
65
|
+
def streaming?
|
66
|
+
@streaming
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Check if polling is enabled
|
71
|
+
#
|
72
|
+
# @return [Boolean] True if polling is enabled
|
73
|
+
def polling?
|
74
|
+
@polling
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Check if client preference should be used for transport negotiation
|
79
|
+
#
|
80
|
+
# @return [Boolean] True if client preference should be used
|
81
|
+
def use_client_preference?
|
82
|
+
@use_client_preference
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# Get the preferred transport protocol
|
87
|
+
#
|
88
|
+
# @return [String] The preferred transport protocol
|
89
|
+
def preferred_transport
|
90
|
+
@supported_transports.first
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# Check if a transport is supported
|
95
|
+
#
|
96
|
+
# @param transport [String] The transport to check
|
97
|
+
# @return [Boolean] True if the transport is supported
|
98
|
+
def supports_transport?(transport)
|
99
|
+
@supported_transports.include?(transport)
|
100
|
+
end
|
101
|
+
|
102
|
+
##
|
103
|
+
# Add a supported transport
|
104
|
+
#
|
105
|
+
# @param transport [String] The transport to add
|
106
|
+
def add_transport(transport)
|
107
|
+
@supported_transports << transport unless @supported_transports.include?(transport)
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# Remove a supported transport
|
112
|
+
#
|
113
|
+
# @param transport [String] The transport to remove
|
114
|
+
def remove_transport(transport)
|
115
|
+
@supported_transports.delete(transport)
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Get authentication configuration for a specific type
|
120
|
+
#
|
121
|
+
# @param type [String] The authentication type
|
122
|
+
# @return [Hash, nil] The authentication configuration
|
123
|
+
def auth_config(type)
|
124
|
+
@authentication[type]
|
125
|
+
end
|
126
|
+
|
127
|
+
##
|
128
|
+
# Set authentication configuration
|
129
|
+
#
|
130
|
+
# @param type [String] The authentication type
|
131
|
+
# @param config [Hash] The authentication configuration
|
132
|
+
def set_auth_config(type, config)
|
133
|
+
@authentication[type] = config
|
134
|
+
end
|
135
|
+
|
136
|
+
##
|
137
|
+
# Get all HTTP headers including authentication headers
|
138
|
+
#
|
139
|
+
# @return [Hash] All HTTP headers
|
140
|
+
def all_headers
|
141
|
+
auth_headers = build_auth_headers
|
142
|
+
@headers.merge(auth_headers)
|
143
|
+
end
|
144
|
+
|
145
|
+
##
|
146
|
+
# Create a copy of the configuration
|
147
|
+
#
|
148
|
+
# @return [Config] A new configuration instance
|
149
|
+
def dup
|
150
|
+
self.class.new(
|
151
|
+
streaming: @streaming,
|
152
|
+
polling: @polling,
|
153
|
+
supported_transports: @supported_transports.dup,
|
154
|
+
use_client_preference: @use_client_preference,
|
155
|
+
accepted_output_modes: @accepted_output_modes.dup,
|
156
|
+
push_notification_configs: @push_notification_configs.dup,
|
157
|
+
timeout: @timeout,
|
158
|
+
retry_attempts: @retry_attempts,
|
159
|
+
retry_delay: @retry_delay,
|
160
|
+
max_retry_delay: @max_retry_delay,
|
161
|
+
backoff_multiplier: @backoff_multiplier,
|
162
|
+
endpoint_url: @endpoint_url,
|
163
|
+
authentication: @authentication.dup,
|
164
|
+
headers: @headers.dup,
|
165
|
+
user_agent: @user_agent
|
166
|
+
)
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def validate!
|
172
|
+
raise ArgumentError, "timeout must be positive" if @timeout <= 0
|
173
|
+
raise ArgumentError, "retry_attempts must be non-negative" if @retry_attempts.negative?
|
174
|
+
raise ArgumentError, "retry_delay must be positive" if @retry_delay <= 0
|
175
|
+
raise ArgumentError, "max_retry_delay must be positive" if @max_retry_delay <= 0
|
176
|
+
raise ArgumentError, "backoff_multiplier must be positive" if @backoff_multiplier <= 0
|
177
|
+
|
178
|
+
@supported_transports.each do |transport|
|
179
|
+
raise ArgumentError, "unsupported transport: #{transport}" unless A2A::Types::VALID_TRANSPORTS.include?(transport)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def build_auth_headers
|
184
|
+
headers = {}
|
185
|
+
|
186
|
+
# Add API key authentication
|
187
|
+
if (api_key_config = @authentication["api_key"])
|
188
|
+
case api_key_config["in"]
|
189
|
+
when "header"
|
190
|
+
headers[api_key_config["name"]] = api_key_config["value"]
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Add bearer token authentication
|
195
|
+
if (bearer_config = @authentication["bearer"])
|
196
|
+
headers["Authorization"] = "Bearer #{bearer_config['token']}"
|
197
|
+
end
|
198
|
+
|
199
|
+
# Add basic authentication
|
200
|
+
if (basic_config = @authentication["basic"])
|
201
|
+
require "base64"
|
202
|
+
credentials = Base64.strict_encode64("#{basic_config['username']}:#{basic_config['password']}")
|
203
|
+
headers["Authorization"] = "Basic #{credentials}"
|
204
|
+
end
|
205
|
+
|
206
|
+
headers
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|