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,524 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "faraday"
|
4
|
+
require "json"
|
5
|
+
require "securerandom"
|
6
|
+
require_relative "base"
|
7
|
+
require_relative "performance_tracker"
|
8
|
+
require_relative "json_rpc_handler"
|
9
|
+
require_relative "api_methods"
|
10
|
+
|
11
|
+
# Try to use connection pooling adapter if available
|
12
|
+
begin
|
13
|
+
require "net/http/persistent"
|
14
|
+
HTTP_ADAPTER = :net_http_persistent
|
15
|
+
rescue LoadError
|
16
|
+
HTTP_ADAPTER = Faraday.default_adapter
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# HTTP client implementation for A2A protocol
|
21
|
+
#
|
22
|
+
# Provides JSON-RPC 2.0 over HTTP(S) communication with A2A agents,
|
23
|
+
# including support for streaming responses via Server-Sent Events.
|
24
|
+
#
|
25
|
+
module A2A
|
26
|
+
module Client
|
27
|
+
class HttpClient < A2A::Client::Base
|
28
|
+
include PerformanceTracker
|
29
|
+
include JsonRpcHandler
|
30
|
+
include ApiMethods
|
31
|
+
|
32
|
+
attr_reader :endpoint_url, :connection
|
33
|
+
|
34
|
+
##
|
35
|
+
# Initialize a new HTTP client
|
36
|
+
#
|
37
|
+
# @param endpoint_url [String] The base URL for the A2A agent
|
38
|
+
# @param config [Config, nil] Client configuration
|
39
|
+
# @param middleware [Array] List of middleware interceptors
|
40
|
+
# @param consumers [Array] List of event consumers
|
41
|
+
def initialize(endpoint_url, config: nil, middleware: [], consumers: [])
|
42
|
+
super(config: config, middleware: middleware, consumers: consumers)
|
43
|
+
@endpoint_url = endpoint_url.chomp("/")
|
44
|
+
@connection = build_connection
|
45
|
+
@connection_pool = nil
|
46
|
+
initialize_performance_tracking
|
47
|
+
initialize_json_rpc_handling
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Get the agent card
|
52
|
+
#
|
53
|
+
# @param context [Hash, nil] Optional context information
|
54
|
+
# @param authenticated [Boolean] Whether to get authenticated extended card
|
55
|
+
# @return [AgentCard] The agent card
|
56
|
+
def get_card(context: nil, authenticated: false)
|
57
|
+
if authenticated
|
58
|
+
request = build_json_rpc_request("agent/getAuthenticatedExtendedCard", {})
|
59
|
+
response = execute_with_middleware(request, context || {}) do |req, _ctx|
|
60
|
+
send_json_rpc_request(req)
|
61
|
+
end
|
62
|
+
ensure_agent_card(response["result"])
|
63
|
+
else
|
64
|
+
# Use HTTP GET for basic agent card
|
65
|
+
response = execute_with_middleware({}, context || {}) do |_req, _ctx|
|
66
|
+
@connection.get("/agent-card") do |request|
|
67
|
+
request.headers.merge!(@config.all_headers)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
if response.success?
|
72
|
+
ensure_agent_card(JSON.parse(response.body))
|
73
|
+
else
|
74
|
+
raise A2A::Errors::HTTPError.new(
|
75
|
+
"Failed to get agent card: #{response.status}",
|
76
|
+
status_code: response.status,
|
77
|
+
response_body: response.body
|
78
|
+
)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Resubscribe to a task for streaming updates
|
85
|
+
#
|
86
|
+
# @param task_id [String] The task ID to resubscribe to
|
87
|
+
# @param context [Hash, nil] Optional context information
|
88
|
+
# @return [Enumerator] Stream of task updates
|
89
|
+
def resubscribe(task_id, context: nil)
|
90
|
+
request = build_json_rpc_request("tasks/resubscribe", { id: task_id })
|
91
|
+
|
92
|
+
execute_with_middleware(request, context || {}) do |req, _ctx|
|
93
|
+
send_streaming_request(req)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# Set a callback for task updates
|
99
|
+
#
|
100
|
+
# @param task_id [String] The task ID
|
101
|
+
# @param push_notification_config [PushNotificationConfig, Hash] The push notification configuration
|
102
|
+
# @param context [Hash, nil] Optional context information
|
103
|
+
# @return [void]
|
104
|
+
def set_task_callback(task_id, push_notification_config, context: nil)
|
105
|
+
config = if push_notification_config.is_a?(A2A::Types::PushNotificationConfig)
|
106
|
+
push_notification_config
|
107
|
+
else
|
108
|
+
A2A::Types::PushNotificationConfig.from_h(push_notification_config)
|
109
|
+
end
|
110
|
+
|
111
|
+
params = {
|
112
|
+
taskId: task_id,
|
113
|
+
pushNotificationConfig: config.to_h
|
114
|
+
}
|
115
|
+
|
116
|
+
request = build_json_rpc_request("tasks/pushNotificationConfig/set", params)
|
117
|
+
execute_with_middleware(request, context || {}) do |req, _ctx|
|
118
|
+
send_json_rpc_request(req)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
##
|
123
|
+
# Get the callback configuration for a task
|
124
|
+
#
|
125
|
+
# @param task_id [String] The task ID
|
126
|
+
# @param push_notification_config_id [String] The push notification config ID
|
127
|
+
# @param context [Hash, nil] Optional context information
|
128
|
+
# @return [TaskPushNotificationConfig] The callback configuration
|
129
|
+
def get_task_callback(task_id, push_notification_config_id, context: nil)
|
130
|
+
params = {
|
131
|
+
taskId: task_id,
|
132
|
+
pushNotificationConfigId: push_notification_config_id
|
133
|
+
}
|
134
|
+
|
135
|
+
request = build_json_rpc_request("tasks/pushNotificationConfig/get", params)
|
136
|
+
response = execute_with_middleware(request, context || {}) do |req, _ctx|
|
137
|
+
send_json_rpc_request(req)
|
138
|
+
end
|
139
|
+
|
140
|
+
A2A::Types::TaskPushNotificationConfig.from_h(response["result"])
|
141
|
+
end
|
142
|
+
|
143
|
+
##
|
144
|
+
# List all callback configurations for a task
|
145
|
+
#
|
146
|
+
# @param task_id [String] The task ID
|
147
|
+
# @param context [Hash, nil] Optional context information
|
148
|
+
# @return [Array<TaskPushNotificationConfig>] List of callback configurations
|
149
|
+
def list_task_callbacks(task_id, context: nil)
|
150
|
+
request = build_json_rpc_request("tasks/pushNotificationConfig/list", { taskId: task_id })
|
151
|
+
response = execute_with_middleware(request, context || {}) do |req, _ctx|
|
152
|
+
send_json_rpc_request(req)
|
153
|
+
end
|
154
|
+
|
155
|
+
response["result"].map { |config| A2A::Types::TaskPushNotificationConfig.from_h(config) }
|
156
|
+
end
|
157
|
+
|
158
|
+
##
|
159
|
+
# Delete a callback configuration for a task
|
160
|
+
#
|
161
|
+
# @param task_id [String] The task ID
|
162
|
+
# @param push_notification_config_id [String] The push notification config ID
|
163
|
+
# @param context [Hash, nil] Optional context information
|
164
|
+
# @return [void]
|
165
|
+
def delete_task_callback(task_id, push_notification_config_id, context: nil)
|
166
|
+
params = {
|
167
|
+
taskId: task_id,
|
168
|
+
pushNotificationConfigId: push_notification_config_id
|
169
|
+
}
|
170
|
+
|
171
|
+
request = build_json_rpc_request("tasks/pushNotificationConfig/delete", params)
|
172
|
+
execute_with_middleware(request, context || {}) do |req, _ctx|
|
173
|
+
send_json_rpc_request(req)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
##
|
178
|
+
# Set push notification configuration for a task
|
179
|
+
#
|
180
|
+
# @param task_id [String] The task ID
|
181
|
+
# @param config [PushNotificationConfig, Hash] The push notification configuration
|
182
|
+
# @param context [Hash, nil] Optional context information
|
183
|
+
# @return [Hash] The created configuration with ID
|
184
|
+
def set_task_push_notification_config(task_id, config, context: nil)
|
185
|
+
# Validate and normalize config
|
186
|
+
normalized_config = if config.is_a?(A2A::Types::PushNotificationConfig)
|
187
|
+
config.to_h
|
188
|
+
elsif config.is_a?(Hash)
|
189
|
+
# Validate required fields
|
190
|
+
raise ArgumentError, "config must include 'url'" unless config[:url] || config["url"]
|
191
|
+
|
192
|
+
config
|
193
|
+
else
|
194
|
+
raise ArgumentError, "config must be a Hash or PushNotificationConfig"
|
195
|
+
end
|
196
|
+
|
197
|
+
params = {
|
198
|
+
taskId: task_id,
|
199
|
+
pushNotificationConfig: normalized_config
|
200
|
+
}
|
201
|
+
|
202
|
+
request = build_json_rpc_request("tasks/pushNotificationConfig/set", params)
|
203
|
+
response = execute_with_middleware(request, context || {}) do |req, _ctx|
|
204
|
+
send_json_rpc_request(req)
|
205
|
+
end
|
206
|
+
|
207
|
+
response["result"]
|
208
|
+
end
|
209
|
+
|
210
|
+
##
|
211
|
+
# Get push notification configuration for a task
|
212
|
+
#
|
213
|
+
# @param task_id [String] The task ID
|
214
|
+
# @param config_id [String] The push notification config ID
|
215
|
+
# @param context [Hash, nil] Optional context information
|
216
|
+
# @return [Hash] The push notification configuration
|
217
|
+
def get_task_push_notification_config(task_id, config_id, context: nil)
|
218
|
+
params = {
|
219
|
+
taskId: task_id,
|
220
|
+
pushNotificationConfigId: config_id
|
221
|
+
}
|
222
|
+
|
223
|
+
request = build_json_rpc_request("tasks/pushNotificationConfig/get", params)
|
224
|
+
response = execute_with_middleware(request, context || {}) do |req, _ctx|
|
225
|
+
send_json_rpc_request(req)
|
226
|
+
end
|
227
|
+
|
228
|
+
response["result"]
|
229
|
+
end
|
230
|
+
|
231
|
+
##
|
232
|
+
# List all push notification configurations for a task
|
233
|
+
#
|
234
|
+
# @param task_id [String] The task ID
|
235
|
+
# @param context [Hash, nil] Optional context information
|
236
|
+
# @return [Array<Hash>] List of push notification configurations
|
237
|
+
def list_task_push_notification_configs(task_id, context: nil)
|
238
|
+
request = build_json_rpc_request("tasks/pushNotificationConfig/list", { taskId: task_id })
|
239
|
+
response = execute_with_middleware(request, context || {}) do |req, _ctx|
|
240
|
+
send_json_rpc_request(req)
|
241
|
+
end
|
242
|
+
|
243
|
+
response["result"] || []
|
244
|
+
end
|
245
|
+
|
246
|
+
##
|
247
|
+
# Delete push notification configuration for a task
|
248
|
+
#
|
249
|
+
# @param task_id [String] The task ID
|
250
|
+
# @param config_id [String] The push notification config ID
|
251
|
+
# @param context [Hash, nil] Optional context information
|
252
|
+
# @return [Boolean] True if deletion was successful
|
253
|
+
def delete_task_push_notification_config(task_id, config_id, context: nil)
|
254
|
+
params = {
|
255
|
+
taskId: task_id,
|
256
|
+
pushNotificationConfigId: config_id
|
257
|
+
}
|
258
|
+
|
259
|
+
request = build_json_rpc_request("tasks/pushNotificationConfig/delete", params)
|
260
|
+
execute_with_middleware(request, context || {}) do |req, _ctx|
|
261
|
+
send_json_rpc_request(req)
|
262
|
+
end
|
263
|
+
|
264
|
+
true
|
265
|
+
end
|
266
|
+
|
267
|
+
private
|
268
|
+
|
269
|
+
##
|
270
|
+
# Build the Faraday connection with performance optimizations
|
271
|
+
#
|
272
|
+
# @return [Faraday::Connection] The configured connection
|
273
|
+
def build_connection
|
274
|
+
Faraday.new(@endpoint_url) do |conn|
|
275
|
+
# Request middleware
|
276
|
+
conn.request :json
|
277
|
+
|
278
|
+
# Response middleware
|
279
|
+
conn.response :json, content_type: /\bjson$/
|
280
|
+
|
281
|
+
# Use connection pooling adapter if available
|
282
|
+
conn.adapter HTTP_ADAPTER
|
283
|
+
|
284
|
+
# Set timeouts
|
285
|
+
conn.options.timeout = @config.timeout
|
286
|
+
conn.options.read_timeout = @config.timeout
|
287
|
+
conn.options.write_timeout = @config.timeout
|
288
|
+
|
289
|
+
# Performance optimizations (only set if supported)
|
290
|
+
conn.options.pool_size = @config.pool_size || 5 if conn.options.respond_to?(:pool_size=)
|
291
|
+
|
292
|
+
# Enable compression if supported
|
293
|
+
conn.headers["Accept-Encoding"] = "gzip, deflate"
|
294
|
+
|
295
|
+
# Set keep-alive headers
|
296
|
+
conn.headers["Connection"] = "keep-alive"
|
297
|
+
conn.headers["Keep-Alive"] = "timeout=30, max=100"
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
##
|
302
|
+
# Send a synchronous message
|
303
|
+
#
|
304
|
+
# @param message [Message] The message to send
|
305
|
+
# @param context [Hash] The request context
|
306
|
+
# @return [Message] The response message
|
307
|
+
def send_sync_message(message, context)
|
308
|
+
request = build_json_rpc_request("message/send", message.to_h)
|
309
|
+
response = execute_with_middleware(request, context) do |req, _ctx|
|
310
|
+
send_json_rpc_request(req)
|
311
|
+
end
|
312
|
+
|
313
|
+
ensure_message(response["result"])
|
314
|
+
end
|
315
|
+
|
316
|
+
##
|
317
|
+
# Send a streaming message
|
318
|
+
#
|
319
|
+
# @param message [Message] The message to send
|
320
|
+
# @param context [Hash] The request context
|
321
|
+
# @return [Enumerator] Stream of response messages
|
322
|
+
def send_streaming_message(message, context)
|
323
|
+
request = build_json_rpc_request("message/stream", message.to_h)
|
324
|
+
|
325
|
+
execute_with_middleware(request, context) do |req, _ctx|
|
326
|
+
send_streaming_request(req)
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
##
|
331
|
+
# Send a JSON-RPC request and get response
|
332
|
+
#
|
333
|
+
# @param request [Hash] The JSON-RPC request
|
334
|
+
# @return [Hash] The JSON-RPC response
|
335
|
+
def send_json_rpc_request(request)
|
336
|
+
response = @connection.post do |req|
|
337
|
+
req.headers.merge!(@config.all_headers)
|
338
|
+
req.headers["Content-Type"] = "application/json"
|
339
|
+
req.body = request.to_json
|
340
|
+
end
|
341
|
+
|
342
|
+
handle_http_response(response)
|
343
|
+
end
|
344
|
+
|
345
|
+
##
|
346
|
+
# Send a streaming request using Server-Sent Events
|
347
|
+
#
|
348
|
+
# @param request [Hash] The JSON-RPC request
|
349
|
+
# @return [Enumerator] Stream of events
|
350
|
+
def send_streaming_request(request)
|
351
|
+
Enumerator.new do |yielder|
|
352
|
+
response = @connection.post do |req|
|
353
|
+
req.headers.merge!(@config.all_headers)
|
354
|
+
req.headers["Content-Type"] = "application/json"
|
355
|
+
req.headers["Accept"] = "text/event-stream"
|
356
|
+
req.body = request.to_json
|
357
|
+
|
358
|
+
# Handle streaming response
|
359
|
+
req.options.on_data = proc do |chunk, _size|
|
360
|
+
events = parse_sse_chunk(chunk)
|
361
|
+
events.each do |event|
|
362
|
+
case event[:event]
|
363
|
+
when "message"
|
364
|
+
yielder << ensure_message(JSON.parse(event[:data]))
|
365
|
+
when "task_status_update"
|
366
|
+
event_data = A2A::Types::TaskStatusUpdateEvent.from_h(JSON.parse(event[:data]))
|
367
|
+
process_event(event_data)
|
368
|
+
yielder << event_data
|
369
|
+
when "task_artifact_update"
|
370
|
+
event_data = A2A::Types::TaskArtifactUpdateEvent.from_h(JSON.parse(event[:data]))
|
371
|
+
process_event(event_data)
|
372
|
+
yielder << event_data
|
373
|
+
when "error"
|
374
|
+
error_data = JSON.parse(event[:data])
|
375
|
+
error = A2A::Errors::ErrorUtils.from_json_rpc_code(
|
376
|
+
error_data["code"],
|
377
|
+
error_data["message"],
|
378
|
+
data: error_data["data"]
|
379
|
+
)
|
380
|
+
raise error
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
unless response.success?
|
387
|
+
raise A2A::Errors::HTTPError.new(
|
388
|
+
"Streaming request failed: #{response.status}",
|
389
|
+
status_code: response.status,
|
390
|
+
response_body: response.body
|
391
|
+
)
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
##
|
397
|
+
# Handle HTTP response and extract JSON-RPC result
|
398
|
+
#
|
399
|
+
# @param response [Faraday::Response] The HTTP response
|
400
|
+
# @return [Hash] The JSON-RPC response
|
401
|
+
def handle_http_response(response)
|
402
|
+
unless response.success?
|
403
|
+
raise A2A::Errors::HTTPError.new(
|
404
|
+
"HTTP request failed: #{response.status}",
|
405
|
+
status_code: response.status,
|
406
|
+
response_body: response.body
|
407
|
+
)
|
408
|
+
end
|
409
|
+
|
410
|
+
begin
|
411
|
+
json_response = response.body.is_a?(Hash) ? response.body : JSON.parse(response.body)
|
412
|
+
rescue JSON::ParserError => e
|
413
|
+
raise A2A::Errors::JSONError, "Invalid JSON response: #{e.message}"
|
414
|
+
end
|
415
|
+
|
416
|
+
# Check for JSON-RPC error
|
417
|
+
if json_response["error"]
|
418
|
+
error = json_response["error"]
|
419
|
+
raise A2A::Errors::ErrorUtils.from_json_rpc_code(
|
420
|
+
error["code"],
|
421
|
+
error["message"],
|
422
|
+
data: error["data"]
|
423
|
+
)
|
424
|
+
end
|
425
|
+
|
426
|
+
json_response
|
427
|
+
end
|
428
|
+
|
429
|
+
##
|
430
|
+
# Parse Server-Sent Events chunk
|
431
|
+
#
|
432
|
+
# @param chunk [String] The SSE chunk
|
433
|
+
# @return [Array<Hash>] Parsed events
|
434
|
+
def parse_sse_chunk(chunk)
|
435
|
+
events = []
|
436
|
+
current_event = {}
|
437
|
+
|
438
|
+
chunk.split("\n").each do |line|
|
439
|
+
line = line.strip
|
440
|
+
next if line.empty?
|
441
|
+
|
442
|
+
if line.start_with?("data: ")
|
443
|
+
current_event[:data] = line[6..]
|
444
|
+
elsif line.start_with?("event: ")
|
445
|
+
current_event[:event] = line[7..]
|
446
|
+
elsif line.start_with?("id: ")
|
447
|
+
current_event[:id] = line[4..]
|
448
|
+
elsif line.start_with?("retry: ")
|
449
|
+
current_event[:retry] = line[7..].to_i
|
450
|
+
elsif line == ""
|
451
|
+
# Empty line indicates end of event
|
452
|
+
events << current_event.dup if current_event[:data]
|
453
|
+
current_event.clear
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
# Handle case where chunk doesn't end with empty line
|
458
|
+
events << current_event if current_event[:data]
|
459
|
+
events
|
460
|
+
end
|
461
|
+
|
462
|
+
##
|
463
|
+
# Build a JSON-RPC request
|
464
|
+
#
|
465
|
+
# @param method [String] The method name
|
466
|
+
# @param params [Hash] The method parameters
|
467
|
+
# @return [A2A::Protocol::Request] The JSON-RPC request
|
468
|
+
def build_json_rpc_request(method, params = {})
|
469
|
+
A2A::Protocol::Request.new(
|
470
|
+
jsonrpc: A2A::Protocol::JsonRpc::JSONRPC_VERSION,
|
471
|
+
method: method,
|
472
|
+
params: params,
|
473
|
+
id: next_request_id
|
474
|
+
)
|
475
|
+
end
|
476
|
+
|
477
|
+
##
|
478
|
+
# Generate next request ID
|
479
|
+
#
|
480
|
+
# @return [Integer] The next request ID
|
481
|
+
def next_request_id
|
482
|
+
@request_id_mutex.synchronize do
|
483
|
+
@request_id_counter += 1
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
##
|
488
|
+
# Get performance statistics
|
489
|
+
#
|
490
|
+
# @return [Hash] Performance statistics
|
491
|
+
def performance_stats
|
492
|
+
@stats_mutex.synchronize { @performance_stats.dup }
|
493
|
+
end
|
494
|
+
|
495
|
+
##
|
496
|
+
# Reset performance statistics
|
497
|
+
#
|
498
|
+
def reset_performance_stats!
|
499
|
+
@stats_mutex.synchronize do
|
500
|
+
@performance_stats = {
|
501
|
+
requests_count: 0,
|
502
|
+
total_time: 0.0,
|
503
|
+
avg_response_time: 0.0,
|
504
|
+
cache_hits: 0,
|
505
|
+
cache_misses: 0
|
506
|
+
}
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
##
|
511
|
+
# Record request performance metrics
|
512
|
+
#
|
513
|
+
# @param duration [Float] Request duration in seconds
|
514
|
+
def record_request_performance(duration)
|
515
|
+
@stats_mutex.synchronize do
|
516
|
+
@performance_stats[:requests_count] += 1
|
517
|
+
@performance_stats[:total_time] += duration
|
518
|
+
@performance_stats[:avg_response_time] =
|
519
|
+
@performance_stats[:total_time] / @performance_stats[:requests_count]
|
520
|
+
end
|
521
|
+
end
|
522
|
+
end
|
523
|
+
end
|
524
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "securerandom"
|
5
|
+
|
6
|
+
module A2A
|
7
|
+
module Client
|
8
|
+
##
|
9
|
+
# JSON-RPC request handling functionality
|
10
|
+
#
|
11
|
+
module JsonRpcHandler
|
12
|
+
##
|
13
|
+
# Initialize JSON-RPC handling
|
14
|
+
#
|
15
|
+
def initialize_json_rpc_handling
|
16
|
+
@request_id_counter = 0
|
17
|
+
@request_id_mutex = Mutex.new
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Send a JSON-RPC request
|
22
|
+
#
|
23
|
+
# @param request [Hash] The JSON-RPC request
|
24
|
+
# @return [Hash] The parsed response
|
25
|
+
def send_json_rpc_request(request)
|
26
|
+
start_time = Time.now
|
27
|
+
response = @connection.post("/", request.to_json, "Content-Type" => "application/json")
|
28
|
+
duration = Time.now - start_time
|
29
|
+
|
30
|
+
record_request_performance(duration)
|
31
|
+
handle_http_response(response)
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Send a streaming JSON-RPC request
|
36
|
+
#
|
37
|
+
# @param request [Hash] The JSON-RPC request
|
38
|
+
# @return [Enumerator] Stream of parsed responses
|
39
|
+
def send_streaming_request(request)
|
40
|
+
Enumerator.new do |yielder|
|
41
|
+
response = @connection.post("/stream", request.to_json) do |req|
|
42
|
+
req.headers["Content-Type"] = "application/json"
|
43
|
+
req.headers["Accept"] = "text/event-stream"
|
44
|
+
end
|
45
|
+
|
46
|
+
raise A2A::Errors::HTTPError, "HTTP #{response.status}: #{response.body}" unless response.success?
|
47
|
+
|
48
|
+
response.body.each_line do |line|
|
49
|
+
event = parse_sse_chunk(line.strip)
|
50
|
+
yielder << event if event
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Handle HTTP response
|
57
|
+
#
|
58
|
+
# @param response [Faraday::Response] The HTTP response
|
59
|
+
# @return [Hash] The parsed JSON-RPC response
|
60
|
+
def handle_http_response(response)
|
61
|
+
unless response.success?
|
62
|
+
case response.status
|
63
|
+
when 408
|
64
|
+
raise A2A::Errors::TimeoutError, "Request timeout"
|
65
|
+
when 400..499
|
66
|
+
raise A2A::Errors::HTTPError, "HTTP #{response.status}: #{response.body}"
|
67
|
+
when 500..599
|
68
|
+
raise A2A::Errors::HTTPError, "HTTP #{response.status}: #{response.body}"
|
69
|
+
else
|
70
|
+
raise A2A::Errors::HTTPError, "HTTP #{response.status}: #{response.body}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
begin
|
75
|
+
parsed_response = JSON.parse(response.body)
|
76
|
+
rescue JSON::ParserError => e
|
77
|
+
raise A2A::Errors::ParseError, "Invalid JSON response: #{e.message}"
|
78
|
+
end
|
79
|
+
|
80
|
+
if parsed_response["error"]
|
81
|
+
error_code = parsed_response["error"]["code"]
|
82
|
+
error_message = parsed_response["error"]["message"]
|
83
|
+
raise A2A::Errors::A2AError.new(error_message, error_code)
|
84
|
+
end
|
85
|
+
|
86
|
+
parsed_response
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# Parse Server-Sent Events chunk
|
91
|
+
#
|
92
|
+
# @param chunk [String] The SSE chunk
|
93
|
+
# @return [Hash, nil] The parsed event or nil
|
94
|
+
def parse_sse_chunk(chunk)
|
95
|
+
return nil if chunk.empty? || chunk.start_with?(":")
|
96
|
+
|
97
|
+
return unless chunk.start_with?("data: ")
|
98
|
+
|
99
|
+
data = chunk[6..] # Remove "data: " prefix
|
100
|
+
return nil if data == "[DONE]"
|
101
|
+
|
102
|
+
begin
|
103
|
+
JSON.parse(data)
|
104
|
+
rescue JSON::ParserError
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
##
|
110
|
+
# Build a JSON-RPC request
|
111
|
+
#
|
112
|
+
# @param method [String] The method name
|
113
|
+
# @param params [Hash] The parameters
|
114
|
+
# @return [Hash] The JSON-RPC request
|
115
|
+
def build_json_rpc_request(method, params = {})
|
116
|
+
{
|
117
|
+
jsonrpc: "2.0",
|
118
|
+
method: method,
|
119
|
+
params: params,
|
120
|
+
id: next_request_id
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# Generate the next request ID
|
126
|
+
#
|
127
|
+
# @return [String] The request ID
|
128
|
+
def next_request_id
|
129
|
+
@request_id_mutex.synchronize do
|
130
|
+
@request_id_counter += 1
|
131
|
+
"#{SecureRandom.hex(8)}-#{@request_id_counter}"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|