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,311 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "rack"
|
5
|
+
require_relative "../request_handler"
|
6
|
+
require_relative "../../protocol/json_rpc"
|
7
|
+
|
8
|
+
module A2A
|
9
|
+
module Server
|
10
|
+
module Apps
|
11
|
+
##
|
12
|
+
# Rack application for serving A2A protocol endpoints
|
13
|
+
#
|
14
|
+
# This class provides a Rack-compatible application that can handle
|
15
|
+
# A2A JSON-RPC requests and serve agent cards. It's similar to the
|
16
|
+
# Python FastAPI implementation but uses Rack for Ruby web servers.
|
17
|
+
#
|
18
|
+
class RackApp
|
19
|
+
AGENT_CARD_PATH = "/.well-known/a2a/agent-card"
|
20
|
+
EXTENDED_AGENT_CARD_PATH = "/a2a/agent-card/extended"
|
21
|
+
RPC_PATH = "/a2a/rpc"
|
22
|
+
|
23
|
+
attr_reader :agent_card, :request_handler, :extended_agent_card
|
24
|
+
|
25
|
+
##
|
26
|
+
# Initialize the Rack application
|
27
|
+
#
|
28
|
+
# @param agent_card [A2A::Types::AgentCard] The agent card describing capabilities
|
29
|
+
# @param request_handler [RequestHandler] The request handler for processing A2A requests
|
30
|
+
# @param extended_agent_card [A2A::Types::AgentCard, nil] Optional extended agent card
|
31
|
+
# @param card_modifier [Proc, nil] Optional callback to modify the public agent card
|
32
|
+
# @param extended_card_modifier [Proc, nil] Optional callback to modify the extended agent card
|
33
|
+
def initialize(agent_card:, request_handler:, extended_agent_card: nil, card_modifier: nil, extended_card_modifier: nil)
|
34
|
+
@agent_card = agent_card
|
35
|
+
@request_handler = request_handler
|
36
|
+
@extended_agent_card = extended_agent_card
|
37
|
+
@card_modifier = card_modifier
|
38
|
+
@extended_card_modifier = extended_card_modifier
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Rack application call method
|
43
|
+
#
|
44
|
+
# @param env [Hash] Rack environment
|
45
|
+
# @return [Array] Rack response [status, headers, body]
|
46
|
+
def call(env)
|
47
|
+
request = Rack::Request.new(env)
|
48
|
+
|
49
|
+
case request.path_info
|
50
|
+
when AGENT_CARD_PATH
|
51
|
+
handle_agent_card(request)
|
52
|
+
when EXTENDED_AGENT_CARD_PATH
|
53
|
+
handle_extended_agent_card(request)
|
54
|
+
when RPC_PATH
|
55
|
+
handle_rpc_request(request)
|
56
|
+
else
|
57
|
+
not_found_response
|
58
|
+
end
|
59
|
+
rescue StandardError => e
|
60
|
+
error_response(500, "Internal Server Error: #{e.message}")
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
##
|
66
|
+
# Handle agent card requests
|
67
|
+
#
|
68
|
+
# @param request [Rack::Request] The request object
|
69
|
+
# @return [Array] Rack response
|
70
|
+
def handle_agent_card(request)
|
71
|
+
return method_not_allowed_response unless request.get?
|
72
|
+
|
73
|
+
card_to_serve = @agent_card
|
74
|
+
card_to_serve = @card_modifier.call(card_to_serve) if @card_modifier
|
75
|
+
|
76
|
+
json_response(200, card_to_serve.to_h)
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# Handle extended agent card requests
|
81
|
+
#
|
82
|
+
# @param request [Rack::Request] The request object
|
83
|
+
# @return [Array] Rack response
|
84
|
+
def handle_extended_agent_card(request)
|
85
|
+
return method_not_allowed_response unless request.get?
|
86
|
+
|
87
|
+
return error_response(404, "Extended agent card not supported") unless @agent_card.supports_authenticated_extended_card
|
88
|
+
|
89
|
+
# Build server context from request
|
90
|
+
context = build_server_context(request)
|
91
|
+
|
92
|
+
card_to_serve = @extended_agent_card || @agent_card
|
93
|
+
|
94
|
+
card_to_serve = @extended_card_modifier.call(card_to_serve, context) if @extended_card_modifier
|
95
|
+
|
96
|
+
json_response(200, card_to_serve.to_h)
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# Handle JSON-RPC requests
|
101
|
+
#
|
102
|
+
# @param request [Rack::Request] The request object
|
103
|
+
# @return [Array] Rack response
|
104
|
+
def handle_rpc_request(request)
|
105
|
+
return method_not_allowed_response unless request.post?
|
106
|
+
|
107
|
+
# Check content type
|
108
|
+
content_type = request.content_type
|
109
|
+
return error_response(400, "Content-Type must be application/json") unless content_type&.include?("application/json")
|
110
|
+
|
111
|
+
# Parse request body
|
112
|
+
body = request.body.read
|
113
|
+
request.body.rewind
|
114
|
+
|
115
|
+
# Parse JSON-RPC request directly from string
|
116
|
+
begin
|
117
|
+
rpc_request = A2A::Protocol::JsonRpc.parse_request(body)
|
118
|
+
rescue A2A::Errors::A2AError => e
|
119
|
+
return json_rpc_error_response(
|
120
|
+
nil, # No ID available if parsing failed
|
121
|
+
e.code,
|
122
|
+
e.message,
|
123
|
+
e.data
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Build server context
|
128
|
+
context = build_server_context(request)
|
129
|
+
|
130
|
+
# Route to appropriate handler method
|
131
|
+
begin
|
132
|
+
result = route_request(rpc_request, context)
|
133
|
+
|
134
|
+
# Handle streaming responses
|
135
|
+
return streaming_response(result) if result.is_a?(Enumerator)
|
136
|
+
|
137
|
+
# Return regular JSON-RPC response
|
138
|
+
response_data = A2A::Protocol::JsonRpc.build_response(
|
139
|
+
result: result,
|
140
|
+
id: rpc_request.id
|
141
|
+
)
|
142
|
+
json_response(200, response_data)
|
143
|
+
rescue A2A::Errors::A2AError => e
|
144
|
+
json_rpc_error_response(rpc_request.id, e.code, e.message, e.data)
|
145
|
+
rescue StandardError => e
|
146
|
+
json_rpc_error_response(
|
147
|
+
rpc_request.id,
|
148
|
+
A2A::Protocol::JsonRpc::INTERNAL_ERROR,
|
149
|
+
"Internal error: #{e.message}"
|
150
|
+
)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
##
|
155
|
+
# Route JSON-RPC request to appropriate handler method
|
156
|
+
#
|
157
|
+
# @param request [A2A::Protocol::Request] The parsed JSON-RPC request
|
158
|
+
# @param context [A2A::Server::Context] The server context
|
159
|
+
# @return [Object] The result from the handler
|
160
|
+
def route_request(request, context)
|
161
|
+
case request.method
|
162
|
+
when "message/send"
|
163
|
+
@request_handler.on_message_send(request.params, context)
|
164
|
+
when "message/stream"
|
165
|
+
@request_handler.on_message_send_stream(request.params, context)
|
166
|
+
when "tasks/get"
|
167
|
+
@request_handler.on_get_task(request.params, context)
|
168
|
+
when "tasks/cancel"
|
169
|
+
@request_handler.on_cancel_task(request.params, context)
|
170
|
+
when "tasks/resubscribe"
|
171
|
+
@request_handler.on_resubscribe_to_task(request.params, context)
|
172
|
+
when "tasks/pushNotificationConfig/set"
|
173
|
+
@request_handler.on_set_task_push_notification_config(request.params, context)
|
174
|
+
when "tasks/pushNotificationConfig/get"
|
175
|
+
@request_handler.on_get_task_push_notification_config(request.params, context)
|
176
|
+
when "tasks/pushNotificationConfig/list"
|
177
|
+
@request_handler.on_list_task_push_notification_config(request.params, context)
|
178
|
+
when "tasks/pushNotificationConfig/delete"
|
179
|
+
@request_handler.on_delete_task_push_notification_config(request.params, context)
|
180
|
+
else
|
181
|
+
raise A2A::Errors::MethodNotFound, "Method '#{request.method}' not found"
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
##
|
186
|
+
# Build server context from Rack request
|
187
|
+
#
|
188
|
+
# @param request [Rack::Request] The Rack request
|
189
|
+
# @return [A2A::Server::Context] The server context
|
190
|
+
def build_server_context(request)
|
191
|
+
context = A2A::Server::Context.new
|
192
|
+
|
193
|
+
# Extract user information if available (depends on authentication middleware)
|
194
|
+
if request.env["warden"]&.authenticated?
|
195
|
+
context.set_user(request.env["warden"].user)
|
196
|
+
context.set_authentication("warden", request.env["warden"])
|
197
|
+
elsif request.env["current_user"]
|
198
|
+
context.set_user(request.env["current_user"])
|
199
|
+
end
|
200
|
+
|
201
|
+
# Set request metadata
|
202
|
+
context.set_metadata(:remote_addr, request.ip)
|
203
|
+
context.set_metadata(:user_agent, request.user_agent)
|
204
|
+
context.set_metadata(:headers, request.env.select { |k, _| k.start_with?("HTTP_") })
|
205
|
+
|
206
|
+
context
|
207
|
+
end
|
208
|
+
|
209
|
+
##
|
210
|
+
# Create a JSON response
|
211
|
+
#
|
212
|
+
# @param status [Integer] HTTP status code
|
213
|
+
# @param data [Object] Data to serialize as JSON
|
214
|
+
# @return [Array] Rack response
|
215
|
+
def json_response(status, data)
|
216
|
+
headers = {
|
217
|
+
"Content-Type" => "application/json",
|
218
|
+
"Cache-Control" => "no-cache"
|
219
|
+
}
|
220
|
+
|
221
|
+
body = JSON.generate(data)
|
222
|
+
[status, headers, [body]]
|
223
|
+
end
|
224
|
+
|
225
|
+
##
|
226
|
+
# Create a JSON-RPC error response
|
227
|
+
#
|
228
|
+
# @param id [String, Integer, nil] Request ID
|
229
|
+
# @param code [Integer] Error code
|
230
|
+
# @param message [String] Error message
|
231
|
+
# @param data [Object, nil] Optional error data
|
232
|
+
# @return [Array] Rack response
|
233
|
+
def json_rpc_error_response(id, code, message, data = nil)
|
234
|
+
error_data = A2A::Protocol::JsonRpc.build_error_response(
|
235
|
+
code: code,
|
236
|
+
message: message,
|
237
|
+
data: data,
|
238
|
+
id: id
|
239
|
+
)
|
240
|
+
json_response(200, error_data)
|
241
|
+
end
|
242
|
+
|
243
|
+
##
|
244
|
+
# Create a streaming response using Server-Sent Events
|
245
|
+
#
|
246
|
+
# @param enumerator [Enumerator] The enumerator yielding events
|
247
|
+
# @return [Array] Rack response
|
248
|
+
def streaming_response(enumerator)
|
249
|
+
headers = {
|
250
|
+
"Content-Type" => "text/event-stream",
|
251
|
+
"Cache-Control" => "no-cache",
|
252
|
+
"Connection" => "keep-alive"
|
253
|
+
}
|
254
|
+
|
255
|
+
# Create streaming body
|
256
|
+
body = Enumerator.new do |yielder|
|
257
|
+
enumerator.each do |event|
|
258
|
+
event_data = if event.respond_to?(:to_h)
|
259
|
+
event.to_h
|
260
|
+
else
|
261
|
+
event
|
262
|
+
end
|
263
|
+
|
264
|
+
yielder << "data: #{JSON.generate(event_data)}\n\n"
|
265
|
+
end
|
266
|
+
rescue StandardError => e
|
267
|
+
error_event = {
|
268
|
+
error: {
|
269
|
+
code: A2A::Protocol::JsonRpc::INTERNAL_ERROR,
|
270
|
+
message: e.message
|
271
|
+
}
|
272
|
+
}
|
273
|
+
yielder << "data: #{JSON.generate(error_event)}\n\n"
|
274
|
+
ensure
|
275
|
+
yielder << "data: [DONE]\n\n"
|
276
|
+
end
|
277
|
+
|
278
|
+
[200, headers, body]
|
279
|
+
end
|
280
|
+
|
281
|
+
##
|
282
|
+
# Create a 404 Not Found response
|
283
|
+
#
|
284
|
+
# @return [Array] Rack response
|
285
|
+
def not_found_response
|
286
|
+
error_response(404, "Not Found")
|
287
|
+
end
|
288
|
+
|
289
|
+
##
|
290
|
+
# Create a 405 Method Not Allowed response
|
291
|
+
#
|
292
|
+
# @return [Array] Rack response
|
293
|
+
def method_not_allowed_response
|
294
|
+
error_response(405, "Method Not Allowed")
|
295
|
+
end
|
296
|
+
|
297
|
+
##
|
298
|
+
# Create an error response
|
299
|
+
#
|
300
|
+
# @param status [Integer] HTTP status code
|
301
|
+
# @param message [String] Error message
|
302
|
+
# @return [Array] Rack response
|
303
|
+
def error_response(status, message)
|
304
|
+
headers = { "Content-Type" => "application/json" }
|
305
|
+
body = JSON.generate({ error: message })
|
306
|
+
[status, headers, [body]]
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
@@ -0,0 +1,261 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "sinatra/base"
|
5
|
+
rescue LoadError
|
6
|
+
raise LoadError, "Sinatra is required for A2A::Server::Apps::SinatraApp. Install with: gem install sinatra"
|
7
|
+
end
|
8
|
+
|
9
|
+
require "json"
|
10
|
+
require_relative "../request_handler"
|
11
|
+
require_relative "../../protocol/json_rpc"
|
12
|
+
|
13
|
+
module A2A
|
14
|
+
module Server
|
15
|
+
module Apps
|
16
|
+
##
|
17
|
+
# Sinatra application for serving A2A protocol endpoints
|
18
|
+
#
|
19
|
+
# This class provides a Sinatra-based application that can handle
|
20
|
+
# A2A JSON-RPC requests and serve agent cards. It's a more Ruby-idiomatic
|
21
|
+
# alternative to the Rack app.
|
22
|
+
#
|
23
|
+
class SinatraApp < Sinatra::Base
|
24
|
+
set :show_exceptions, false
|
25
|
+
set :raise_errors, false
|
26
|
+
|
27
|
+
class << self
|
28
|
+
attr_accessor :agent_card, :request_handler, :extended_agent_card, :card_modifier, :extended_card_modifier
|
29
|
+
|
30
|
+
##
|
31
|
+
# Configure the Sinatra app with A2A components
|
32
|
+
#
|
33
|
+
# @param agent_card [A2A::Types::AgentCard] The agent card
|
34
|
+
# @param request_handler [RequestHandler] The request handler
|
35
|
+
# @param extended_agent_card [A2A::Types::AgentCard, nil] Optional extended agent card
|
36
|
+
# @param card_modifier [Proc, nil] Optional card modifier
|
37
|
+
# @param extended_card_modifier [Proc, nil] Optional extended card modifier
|
38
|
+
def configure_a2a(agent_card:, request_handler:, extended_agent_card: nil, card_modifier: nil,
|
39
|
+
extended_card_modifier: nil)
|
40
|
+
self.agent_card = agent_card
|
41
|
+
self.request_handler = request_handler
|
42
|
+
self.extended_agent_card = extended_agent_card
|
43
|
+
self.card_modifier = card_modifier
|
44
|
+
self.extended_card_modifier = extended_card_modifier
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Agent card endpoint
|
49
|
+
get "/.well-known/a2a/agent-card" do
|
50
|
+
content_type :json
|
51
|
+
|
52
|
+
card_to_serve = self.class.agent_card
|
53
|
+
card_to_serve = self.class.card_modifier.call(card_to_serve) if self.class.card_modifier
|
54
|
+
|
55
|
+
JSON.generate(card_to_serve.to_h)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Extended agent card endpoint
|
59
|
+
get "/a2a/agent-card/extended" do
|
60
|
+
content_type :json
|
61
|
+
|
62
|
+
unless self.class.agent_card.supports_authenticated_extended_card
|
63
|
+
halt 404, JSON.generate({ error: "Extended agent card not supported" })
|
64
|
+
end
|
65
|
+
|
66
|
+
# Build server context
|
67
|
+
context = build_server_context
|
68
|
+
|
69
|
+
card_to_serve = self.class.extended_agent_card || self.class.agent_card
|
70
|
+
|
71
|
+
card_to_serve = self.class.extended_card_modifier.call(card_to_serve, context) if self.class.extended_card_modifier
|
72
|
+
|
73
|
+
JSON.generate(card_to_serve.to_h)
|
74
|
+
end
|
75
|
+
|
76
|
+
# JSON-RPC endpoint
|
77
|
+
post "/a2a/rpc" do
|
78
|
+
content_type :json
|
79
|
+
|
80
|
+
# Validate content type
|
81
|
+
unless request.content_type&.include?("application/json")
|
82
|
+
halt 400, JSON.generate({ error: "Content-Type must be application/json" })
|
83
|
+
end
|
84
|
+
|
85
|
+
# Parse request body
|
86
|
+
begin
|
87
|
+
request.body.rewind
|
88
|
+
json_data = JSON.parse(request.body.read)
|
89
|
+
rescue JSON::ParserError => e
|
90
|
+
return json_rpc_error_response(nil, A2A::Protocol::JsonRpc::PARSE_ERROR, "Parse error: #{e.message}")
|
91
|
+
end
|
92
|
+
|
93
|
+
# Validate JSON-RPC structure
|
94
|
+
begin
|
95
|
+
rpc_request = A2A::Protocol::JsonRpc.parse_request(json_data)
|
96
|
+
rescue A2A::Errors::A2AError => e
|
97
|
+
return json_rpc_error_response(json_data["id"], e.code, e.message, e.data)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Build server context
|
101
|
+
context = build_server_context
|
102
|
+
|
103
|
+
# Route to appropriate handler method
|
104
|
+
begin
|
105
|
+
result = route_request(rpc_request, context)
|
106
|
+
|
107
|
+
# Handle streaming responses
|
108
|
+
return handle_streaming_response(result) if result.is_a?(Enumerator)
|
109
|
+
|
110
|
+
# Return regular JSON-RPC response
|
111
|
+
response_data = A2A::Protocol::JsonRpc.build_response(
|
112
|
+
result: result,
|
113
|
+
id: rpc_request.id
|
114
|
+
)
|
115
|
+
JSON.generate(response_data)
|
116
|
+
rescue A2A::Errors::A2AError => e
|
117
|
+
json_rpc_error_response(rpc_request.id, e.code, e.message, e.data)
|
118
|
+
rescue StandardError => e
|
119
|
+
json_rpc_error_response(
|
120
|
+
rpc_request.id,
|
121
|
+
A2A::Protocol::JsonRpc::INTERNAL_ERROR,
|
122
|
+
"Internal error: #{e.message}"
|
123
|
+
)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Error handlers
|
128
|
+
error A2A::Errors::A2AError do
|
129
|
+
content_type :json
|
130
|
+
error = env["sinatra.error"]
|
131
|
+
status 400
|
132
|
+
JSON.generate({
|
133
|
+
error: {
|
134
|
+
code: error.code,
|
135
|
+
message: error.message,
|
136
|
+
data: error.data
|
137
|
+
}
|
138
|
+
})
|
139
|
+
end
|
140
|
+
|
141
|
+
error StandardError do
|
142
|
+
content_type :json
|
143
|
+
error = env["sinatra.error"]
|
144
|
+
status 500
|
145
|
+
JSON.generate({
|
146
|
+
error: {
|
147
|
+
code: A2A::Protocol::JsonRpc::INTERNAL_ERROR,
|
148
|
+
message: "Internal Server Error: #{error.message}"
|
149
|
+
}
|
150
|
+
})
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
##
|
156
|
+
# Route JSON-RPC request to appropriate handler method
|
157
|
+
#
|
158
|
+
# @param request [A2A::Protocol::Request] The parsed JSON-RPC request
|
159
|
+
# @param context [A2A::Server::Context] The server context
|
160
|
+
# @return [Object] The result from the handler
|
161
|
+
def route_request(request, context)
|
162
|
+
case request.method
|
163
|
+
when "message/send"
|
164
|
+
self.class.request_handler.on_message_send(request.params, context)
|
165
|
+
when "message/stream"
|
166
|
+
self.class.request_handler.on_message_send_stream(request.params, context)
|
167
|
+
when "tasks/get"
|
168
|
+
self.class.request_handler.on_get_task(request.params, context)
|
169
|
+
when "tasks/cancel"
|
170
|
+
self.class.request_handler.on_cancel_task(request.params, context)
|
171
|
+
when "tasks/resubscribe"
|
172
|
+
self.class.request_handler.on_resubscribe_to_task(request.params, context)
|
173
|
+
when "tasks/pushNotificationConfig/set"
|
174
|
+
self.class.request_handler.on_set_task_push_notification_config(request.params, context)
|
175
|
+
when "tasks/pushNotificationConfig/get"
|
176
|
+
self.class.request_handler.on_get_task_push_notification_config(request.params, context)
|
177
|
+
when "tasks/pushNotificationConfig/list"
|
178
|
+
self.class.request_handler.on_list_task_push_notification_config(request.params, context)
|
179
|
+
when "tasks/pushNotificationConfig/delete"
|
180
|
+
self.class.request_handler.on_delete_task_push_notification_config(request.params, context)
|
181
|
+
else
|
182
|
+
raise A2A::Errors::MethodNotFound, "Method '#{request.method}' not found"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
##
|
187
|
+
# Build server context from Sinatra request
|
188
|
+
#
|
189
|
+
# @return [A2A::Server::Context] The server context
|
190
|
+
def build_server_context
|
191
|
+
context = A2A::Server::Context.new
|
192
|
+
|
193
|
+
# Extract user information if available
|
194
|
+
if respond_to?(:current_user) && current_user
|
195
|
+
context.set_user(current_user)
|
196
|
+
elsif env["warden"]&.authenticated?
|
197
|
+
context.set_user(env["warden"].user)
|
198
|
+
context.set_authentication("warden", env["warden"])
|
199
|
+
end
|
200
|
+
|
201
|
+
# Set request metadata
|
202
|
+
context.set_metadata(:remote_addr, request.ip)
|
203
|
+
context.set_metadata(:user_agent, request.user_agent)
|
204
|
+
context.set_metadata(:headers, request.env.select { |k, _| k.start_with?("HTTP_") })
|
205
|
+
|
206
|
+
context
|
207
|
+
end
|
208
|
+
|
209
|
+
##
|
210
|
+
# Create a JSON-RPC error response
|
211
|
+
#
|
212
|
+
# @param id [String, Integer, nil] Request ID
|
213
|
+
# @param code [Integer] Error code
|
214
|
+
# @param message [String] Error message
|
215
|
+
# @param data [Object, nil] Optional error data
|
216
|
+
# @return [String] JSON response
|
217
|
+
def json_rpc_error_response(id, code, message, data = nil)
|
218
|
+
error_data = A2A::Protocol::JsonRpc.build_error_response(
|
219
|
+
code: code,
|
220
|
+
message: message,
|
221
|
+
data: data,
|
222
|
+
id: id
|
223
|
+
)
|
224
|
+
JSON.generate(error_data)
|
225
|
+
end
|
226
|
+
|
227
|
+
##
|
228
|
+
# Handle streaming response using Server-Sent Events
|
229
|
+
#
|
230
|
+
# @param enumerator [Enumerator] The enumerator yielding events
|
231
|
+
# @return [String] SSE response
|
232
|
+
def handle_streaming_response(enumerator)
|
233
|
+
content_type "text/event-stream"
|
234
|
+
headers "Cache-Control" => "no-cache", "Connection" => "keep-alive"
|
235
|
+
|
236
|
+
stream do |out|
|
237
|
+
enumerator.each do |event|
|
238
|
+
event_data = if event.respond_to?(:to_h)
|
239
|
+
event.to_h
|
240
|
+
else
|
241
|
+
event
|
242
|
+
end
|
243
|
+
|
244
|
+
out << "data: #{JSON.generate(event_data)}\n\n"
|
245
|
+
end
|
246
|
+
rescue StandardError => e
|
247
|
+
error_event = {
|
248
|
+
error: {
|
249
|
+
code: A2A::Protocol::JsonRpc::INTERNAL_ERROR,
|
250
|
+
message: e.message
|
251
|
+
}
|
252
|
+
}
|
253
|
+
out << "data: #{JSON.generate(error_event)}\n\n"
|
254
|
+
ensure
|
255
|
+
out << "data: [DONE]\n\n"
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|