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,248 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "agent"
|
4
|
+
require_relative "a2a_methods"
|
5
|
+
require_relative "../types"
|
6
|
+
|
7
|
+
##
|
8
|
+
# Example agent implementation demonstrating A2A protocol methods
|
9
|
+
#
|
10
|
+
# This class shows how to create an A2A agent that includes the standard
|
11
|
+
# protocol methods and implements custom message processing logic.
|
12
|
+
#
|
13
|
+
module A2A
|
14
|
+
module Server
|
15
|
+
class ExampleAgent
|
16
|
+
include A2A::Server::Agent
|
17
|
+
include A2A::Server::A2AMethods
|
18
|
+
|
19
|
+
# Configure the agent
|
20
|
+
a2a_config name: "Example A2A Agent",
|
21
|
+
description: "A demonstration agent for the A2A protocol",
|
22
|
+
version: "1.0.0",
|
23
|
+
default_input_modes: ["text"],
|
24
|
+
default_output_modes: ["text"]
|
25
|
+
|
26
|
+
# Define a simple capability
|
27
|
+
a2a_capability "echo" do
|
28
|
+
method :echo
|
29
|
+
description "Echo back the input message"
|
30
|
+
input_schema type: "object",
|
31
|
+
properties: { message: { type: "string" } },
|
32
|
+
required: ["message"]
|
33
|
+
output_schema type: "object",
|
34
|
+
properties: { echo: { type: "string" } }
|
35
|
+
tags %w[utility test]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Define a custom method
|
39
|
+
a2a_method "echo" do |params, _context|
|
40
|
+
message = params["message"]
|
41
|
+
{ echo: "You said: #{message}" }
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
##
|
47
|
+
# Process message synchronously
|
48
|
+
#
|
49
|
+
# @param message [A2A::Types::Message] The message to process
|
50
|
+
# @param task [A2A::Types::Task] The associated task
|
51
|
+
# @param context [A2A::Server::Context] Request context
|
52
|
+
# @return [Object] Processing result
|
53
|
+
def process_message_sync(message, task, _context)
|
54
|
+
# Extract text from message parts
|
55
|
+
text_parts = message.parts.select { |part| part.is_a?(A2A::Types::TextPart) }
|
56
|
+
text_content = text_parts.map(&:text).join(" ")
|
57
|
+
|
58
|
+
# Simple echo response
|
59
|
+
response_message = A2A::Types::Message.new(
|
60
|
+
message_id: SecureRandom.uuid,
|
61
|
+
role: A2A::Types::ROLE_AGENT,
|
62
|
+
parts: [
|
63
|
+
A2A::Types::TextPart.new(text: "Echo: #{text_content}")
|
64
|
+
],
|
65
|
+
context_id: message.context_id,
|
66
|
+
task_id: task.id
|
67
|
+
)
|
68
|
+
|
69
|
+
# Add message to task history
|
70
|
+
task_manager.add_message(task.id, message)
|
71
|
+
task_manager.add_message(task.id, response_message)
|
72
|
+
|
73
|
+
{
|
74
|
+
message: response_message.to_h,
|
75
|
+
processed_at: Time.now.utc.iso8601
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# Process message asynchronously
|
81
|
+
#
|
82
|
+
# @param message [A2A::Types::Message] The message to process
|
83
|
+
# @param task [A2A::Types::Task] The associated task
|
84
|
+
# @param context [A2A::Server::Context] Request context
|
85
|
+
# @return [void]
|
86
|
+
def process_message_async(message, task, context)
|
87
|
+
# Start background processing
|
88
|
+
Thread.new do
|
89
|
+
# Simulate some processing time
|
90
|
+
sleep 1
|
91
|
+
|
92
|
+
# Process the message
|
93
|
+
result = process_message_sync(message, task, context)
|
94
|
+
|
95
|
+
# Update task with result
|
96
|
+
task_manager.update_task_status(
|
97
|
+
task.id,
|
98
|
+
A2A::Types::TaskStatus.new(
|
99
|
+
state: A2A::Types::TASK_STATE_COMPLETED,
|
100
|
+
result: result,
|
101
|
+
updated_at: Time.now.utc.iso8601
|
102
|
+
)
|
103
|
+
)
|
104
|
+
rescue StandardError => e
|
105
|
+
# Handle errors
|
106
|
+
task_manager.update_task_status(
|
107
|
+
task.id,
|
108
|
+
A2A::Types::TaskStatus.new(
|
109
|
+
state: A2A::Types::TASK_STATE_FAILED,
|
110
|
+
error: { message: e.message, type: e.class.name },
|
111
|
+
updated_at: Time.now.utc.iso8601
|
112
|
+
)
|
113
|
+
)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
##
|
118
|
+
# Process message stream
|
119
|
+
#
|
120
|
+
# @param message [A2A::Types::Message] The message to process
|
121
|
+
# @param task [A2A::Types::Task] The associated task
|
122
|
+
# @param context [A2A::Server::Context] Request context
|
123
|
+
# @yield [response] Yields each response in the stream
|
124
|
+
# @return [void]
|
125
|
+
def process_message_stream(message, task, _context)
|
126
|
+
# Extract text from message parts
|
127
|
+
text_parts = message.parts.select { |part| part.is_a?(A2A::Types::TextPart) }
|
128
|
+
text_content = text_parts.map(&:text).join(" ")
|
129
|
+
|
130
|
+
# Stream back the message word by word
|
131
|
+
words = text_content.split(/\s+/)
|
132
|
+
|
133
|
+
words.each_with_index do |word, index|
|
134
|
+
response_message = A2A::Types::Message.new(
|
135
|
+
message_id: SecureRandom.uuid,
|
136
|
+
role: A2A::Types::ROLE_AGENT,
|
137
|
+
parts: [
|
138
|
+
A2A::Types::TextPart.new(text: "Word #{index + 1}: #{word}")
|
139
|
+
],
|
140
|
+
context_id: message.context_id,
|
141
|
+
task_id: task.id
|
142
|
+
)
|
143
|
+
|
144
|
+
yield response_message.to_h
|
145
|
+
|
146
|
+
# Small delay between words
|
147
|
+
sleep 0.5
|
148
|
+
end
|
149
|
+
|
150
|
+
# Final message
|
151
|
+
final_message = A2A::Types::Message.new(
|
152
|
+
message_id: SecureRandom.uuid,
|
153
|
+
role: A2A::Types::ROLE_AGENT,
|
154
|
+
parts: [
|
155
|
+
A2A::Types::TextPart.new(text: "Streaming complete. Processed #{words.length} words.")
|
156
|
+
],
|
157
|
+
context_id: message.context_id,
|
158
|
+
task_id: task.id
|
159
|
+
)
|
160
|
+
|
161
|
+
yield final_message.to_h
|
162
|
+
end
|
163
|
+
|
164
|
+
##
|
165
|
+
# Generate agent card
|
166
|
+
#
|
167
|
+
# @param context [A2A::Server::Context] Request context
|
168
|
+
# @return [A2A::Types::AgentCard] The agent card
|
169
|
+
def generate_agent_card(_context)
|
170
|
+
A2A::Types::AgentCard.new(
|
171
|
+
name: self.class._a2a_config[:name] || "Example Agent",
|
172
|
+
description: self.class._a2a_config[:description] || "An example A2A agent",
|
173
|
+
version: self.class._a2a_config[:version] || "1.0.0",
|
174
|
+
url: "https://example.com/agent",
|
175
|
+
preferred_transport: A2A::Types::TRANSPORT_JSONRPC,
|
176
|
+
skills: generate_skills_from_capabilities,
|
177
|
+
capabilities: generate_capabilities_info,
|
178
|
+
default_input_modes: self.class._a2a_config[:default_input_modes] || ["text"],
|
179
|
+
default_output_modes: self.class._a2a_config[:default_output_modes] || ["text"],
|
180
|
+
additional_interfaces: [
|
181
|
+
A2A::Types::AgentInterface.new(
|
182
|
+
transport: A2A::Types::TRANSPORT_JSONRPC,
|
183
|
+
url: "https://example.com/agent/rpc"
|
184
|
+
)
|
185
|
+
],
|
186
|
+
supports_authenticated_extended_card: true,
|
187
|
+
protocol_version: "1.0"
|
188
|
+
)
|
189
|
+
end
|
190
|
+
|
191
|
+
##
|
192
|
+
# Generate extended agent card with authentication context
|
193
|
+
#
|
194
|
+
# @param context [A2A::Server::Context] Request context
|
195
|
+
# @return [A2A::Types::AgentCard] The extended agent card
|
196
|
+
def generate_extended_agent_card(context)
|
197
|
+
# Get base card
|
198
|
+
card = generate_agent_card(context)
|
199
|
+
|
200
|
+
# Add authenticated user information if available
|
201
|
+
if context.user
|
202
|
+
# Modify card based on user context
|
203
|
+
# This is where you could add user-specific capabilities or information
|
204
|
+
card.instance_variable_set(:@metadata, {
|
205
|
+
authenticated_user: context.user.to_s,
|
206
|
+
authentication_time: Time.now.utc.iso8601,
|
207
|
+
extended_features: %w[user_context personalized_responses]
|
208
|
+
})
|
209
|
+
end
|
210
|
+
|
211
|
+
card
|
212
|
+
end
|
213
|
+
|
214
|
+
private
|
215
|
+
|
216
|
+
##
|
217
|
+
# Generate skills from registered capabilities
|
218
|
+
#
|
219
|
+
# @return [Array<A2A::Types::AgentSkill>] List of skills
|
220
|
+
def generate_skills_from_capabilities
|
221
|
+
self.class.a2a_capability_registry.all.map do |capability|
|
222
|
+
A2A::Types::AgentSkill.new(
|
223
|
+
id: capability.name,
|
224
|
+
name: capability.name.humanize,
|
225
|
+
description: capability.description || "No description available",
|
226
|
+
tags: capability.tags || [],
|
227
|
+
examples: capability.examples || [],
|
228
|
+
input_modes: ["text"],
|
229
|
+
output_modes: ["text"]
|
230
|
+
)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
##
|
235
|
+
# Generate capabilities information
|
236
|
+
#
|
237
|
+
# @return [A2A::Types::AgentCapabilities] Capabilities info
|
238
|
+
def generate_capabilities_info
|
239
|
+
A2A::Types::AgentCapabilities.new(
|
240
|
+
streaming: true,
|
241
|
+
push_notifications: true,
|
242
|
+
state_transition_history: true,
|
243
|
+
extensions: []
|
244
|
+
)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../protocol/json_rpc"
|
4
|
+
require_relative "../errors"
|
5
|
+
require_relative "agent"
|
6
|
+
require_relative "middleware"
|
7
|
+
|
8
|
+
##
|
9
|
+
# Request handler for processing A2A JSON-RPC requests
|
10
|
+
#
|
11
|
+
# This class handles the processing of JSON-RPC requests, including
|
12
|
+
# method routing, parameter validation, response generation, and
|
13
|
+
# batch request processing.
|
14
|
+
#
|
15
|
+
# @example Basic usage
|
16
|
+
# handler = A2A::Server::Handler.new(agent_instance)
|
17
|
+
# response = handler.handle_request(json_request_string)
|
18
|
+
#
|
19
|
+
module A2A
|
20
|
+
module Server
|
21
|
+
class Handler
|
22
|
+
attr_reader :agent, :middleware_stack
|
23
|
+
|
24
|
+
##
|
25
|
+
# Initialize a new request handler
|
26
|
+
#
|
27
|
+
# @param agent [Object] The agent instance that includes A2A::Server::Agent
|
28
|
+
# @param middleware [Array] Array of middleware to apply
|
29
|
+
def initialize(agent, middleware: [])
|
30
|
+
@agent = agent
|
31
|
+
@middleware_stack = middleware.dup
|
32
|
+
|
33
|
+
validate_agent!
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Handle a JSON-RPC request string
|
38
|
+
#
|
39
|
+
# @param request_body [String] The JSON-RPC request as a string
|
40
|
+
# @param context [A2A::Server::Context, nil] Optional request context
|
41
|
+
# @return [String] The JSON-RPC response as a string
|
42
|
+
def handle_request(request_body, context: nil)
|
43
|
+
A2A::Monitoring::Instrumentation.instrument_request({ method: "parse_request" }) do
|
44
|
+
# Parse the JSON-RPC request
|
45
|
+
parsed_request = A2A::Protocol::JsonRpc.parse_request(request_body)
|
46
|
+
|
47
|
+
# Handle single or batch requests
|
48
|
+
if parsed_request.is_a?(Array)
|
49
|
+
handle_batch_request(parsed_request, context: context)
|
50
|
+
else
|
51
|
+
handle_single_request(parsed_request, context: context)
|
52
|
+
end
|
53
|
+
rescue A2A::Errors::A2AError => e
|
54
|
+
# Return error response for A2A errors
|
55
|
+
error_response = A2A::Protocol::JsonRpc.build_error_response(
|
56
|
+
code: e.code,
|
57
|
+
message: e.message,
|
58
|
+
data: e.data,
|
59
|
+
id: nil # Unknown ID for parse errors
|
60
|
+
)
|
61
|
+
error_response.to_json
|
62
|
+
rescue StandardError => e
|
63
|
+
# Return internal error for unexpected errors
|
64
|
+
error_response = A2A::Protocol::JsonRpc.build_error_response(
|
65
|
+
code: A2A::Protocol::JsonRpc::INTERNAL_ERROR,
|
66
|
+
message: "Internal server error: #{e.message}",
|
67
|
+
id: nil
|
68
|
+
)
|
69
|
+
error_response.to_json
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Handle a single JSON-RPC request
|
75
|
+
#
|
76
|
+
# @param request [A2A::Protocol::Request] The parsed request
|
77
|
+
# @param context [A2A::Server::Context, nil] Optional request context
|
78
|
+
# @return [String] The JSON-RPC response as a string
|
79
|
+
def handle_single_request(request, context: nil)
|
80
|
+
# Apply middleware stack
|
81
|
+
response = apply_middleware_stack(request, context) do
|
82
|
+
# Route the request to the agent
|
83
|
+
route_request(request, context: context)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Convert response to JSON
|
87
|
+
if response
|
88
|
+
response.to_json
|
89
|
+
else
|
90
|
+
# No response for notifications
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# Handle a batch JSON-RPC request
|
97
|
+
#
|
98
|
+
# @param requests [Array<A2A::Protocol::Request>] Array of parsed requests
|
99
|
+
# @param context [A2A::Server::Context, nil] Optional request context
|
100
|
+
# @return [String] The JSON-RPC batch response as a string
|
101
|
+
def handle_batch_request(requests, context: nil)
|
102
|
+
# Process each request in the batch
|
103
|
+
responses = requests.map do |request|
|
104
|
+
# Apply middleware stack for each request
|
105
|
+
apply_middleware_stack(request, context) do
|
106
|
+
route_request(request, context: context)
|
107
|
+
end
|
108
|
+
rescue StandardError => e
|
109
|
+
# Convert errors to error responses
|
110
|
+
A2A::Errors::ErrorUtils.exception_to_json_rpc_error(e, request_id: request.id)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Filter out nil responses (from notifications)
|
114
|
+
batch_response = A2A::Protocol::JsonRpc.build_batch_response(responses.compact)
|
115
|
+
|
116
|
+
# Return empty array if no responses (all notifications)
|
117
|
+
batch_response.to_json
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# Route a request to the appropriate agent method
|
122
|
+
#
|
123
|
+
# @param request [A2A::Protocol::Request] The JSON-RPC request
|
124
|
+
# @param context [A2A::Server::Context, nil] Optional request context
|
125
|
+
# @return [Hash, nil] The JSON-RPC response hash or nil for notifications
|
126
|
+
def route_request(request, context: nil)
|
127
|
+
# Validate the request
|
128
|
+
validate_request(request)
|
129
|
+
|
130
|
+
# Create or enhance context
|
131
|
+
request_context = context || A2A::Server::Context.new(request: request)
|
132
|
+
request_context.instance_variable_set(:@request, request) if context
|
133
|
+
|
134
|
+
# Check if the method exists
|
135
|
+
unless @agent.class.a2a_method_registered?(request.method)
|
136
|
+
raise A2A::Errors::MethodNotFound, "Method '#{request.method}' not found"
|
137
|
+
end
|
138
|
+
|
139
|
+
# Get method definition for validation
|
140
|
+
@agent.class.a2a_method_definition(request.method)
|
141
|
+
|
142
|
+
# Validate parameters against capability schema if available
|
143
|
+
validate_method_parameters(request.method, request.params)
|
144
|
+
|
145
|
+
# Delegate to the agent
|
146
|
+
@agent.handle_a2a_request(request, context: request_context)
|
147
|
+
end
|
148
|
+
|
149
|
+
##
|
150
|
+
# Add middleware to the handler
|
151
|
+
#
|
152
|
+
# @param middleware [Object] Middleware instance that responds to #call
|
153
|
+
def add_middleware(middleware)
|
154
|
+
@middleware_stack << middleware
|
155
|
+
end
|
156
|
+
|
157
|
+
##
|
158
|
+
# Remove middleware from the handler
|
159
|
+
#
|
160
|
+
# @param middleware [Object] Middleware instance to remove
|
161
|
+
def remove_middleware(middleware)
|
162
|
+
@middleware_stack.delete(middleware)
|
163
|
+
end
|
164
|
+
|
165
|
+
##
|
166
|
+
# Get all registered methods from the agent
|
167
|
+
#
|
168
|
+
# @return [Array<String>] Array of method names
|
169
|
+
def registered_methods
|
170
|
+
@agent.class.a2a_method_registry.keys
|
171
|
+
end
|
172
|
+
|
173
|
+
##
|
174
|
+
# Check if a method is registered
|
175
|
+
#
|
176
|
+
# @param method_name [String] The method name to check
|
177
|
+
# @return [Boolean] True if the method is registered
|
178
|
+
def method_registered?(method_name)
|
179
|
+
@agent.class.a2a_method_registered?(method_name)
|
180
|
+
end
|
181
|
+
|
182
|
+
##
|
183
|
+
# Get method definition
|
184
|
+
#
|
185
|
+
# @param method_name [String] The method name
|
186
|
+
# @return [Hash, nil] The method definition or nil if not found
|
187
|
+
def method_definition(method_name)
|
188
|
+
@agent.class.a2a_method_definition(method_name)
|
189
|
+
end
|
190
|
+
|
191
|
+
##
|
192
|
+
# Get all capabilities from the agent
|
193
|
+
#
|
194
|
+
# @return [Array<A2A::Protocol::Capability>] Array of capabilities
|
195
|
+
def capabilities
|
196
|
+
@agent.class.a2a_capability_registry.all
|
197
|
+
end
|
198
|
+
|
199
|
+
##
|
200
|
+
# Find capability by method name
|
201
|
+
#
|
202
|
+
# @param method_name [String] The method name
|
203
|
+
# @return [A2A::Protocol::Capability, nil] The capability or nil if not found
|
204
|
+
def find_capability_by_method(method_name)
|
205
|
+
@agent.class.a2a_capability_registry.find_by_method(method_name).first
|
206
|
+
end
|
207
|
+
|
208
|
+
private
|
209
|
+
|
210
|
+
##
|
211
|
+
# Validate that the agent includes the Agent module
|
212
|
+
def validate_agent!
|
213
|
+
return if @agent.class.included_modules.include?(A2A::Server::Agent)
|
214
|
+
|
215
|
+
raise ArgumentError, "Agent must include A2A::Server::Agent module"
|
216
|
+
end
|
217
|
+
|
218
|
+
##
|
219
|
+
# Validate a JSON-RPC request
|
220
|
+
#
|
221
|
+
# @param request [A2A::Protocol::Request] The request to validate
|
222
|
+
# @raise [A2A::Errors::InvalidRequest] If the request is invalid
|
223
|
+
def validate_request(request)
|
224
|
+
raise A2A::Errors::InvalidRequest, "Invalid request object" unless request.is_a?(A2A::Protocol::Request)
|
225
|
+
|
226
|
+
if request.method.nil? || (respond_to?(:empty?) && empty?) || (is_a?(String) && strip.empty?)
|
227
|
+
raise A2A::Errors::InvalidRequest,
|
228
|
+
"Method name is required"
|
229
|
+
end
|
230
|
+
|
231
|
+
return if request.params.is_a?(Hash) || request.params.is_a?(Array)
|
232
|
+
|
233
|
+
raise A2A::Errors::InvalidParams, "Parameters must be an object or array"
|
234
|
+
end
|
235
|
+
|
236
|
+
##
|
237
|
+
# Validate method parameters against capability schema
|
238
|
+
#
|
239
|
+
# @param method_name [String] The method name
|
240
|
+
# @param params [Hash, Array] The method parameters
|
241
|
+
# @raise [A2A::Errors::InvalidParams] If parameters are invalid
|
242
|
+
def validate_method_parameters(method_name, params)
|
243
|
+
capability = find_capability_by_method(method_name)
|
244
|
+
return unless capability # Skip validation if no capability defined
|
245
|
+
|
246
|
+
begin
|
247
|
+
capability.validate_input(params)
|
248
|
+
rescue ArgumentError => e
|
249
|
+
raise A2A::Errors::InvalidParams, "Parameter validation failed: #{e.message}"
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
##
|
254
|
+
# Apply the middleware stack to a request
|
255
|
+
#
|
256
|
+
# @param request [A2A::Protocol::Request] The request
|
257
|
+
# @param context [A2A::Server::Context, nil] The request context
|
258
|
+
# @yield Block to execute after middleware
|
259
|
+
# @return [Object] The result from the block
|
260
|
+
def apply_middleware_stack(request, context, &block)
|
261
|
+
# Build the middleware chain from the outside in
|
262
|
+
chain = block
|
263
|
+
|
264
|
+
@middleware_stack.reverse_each do |middleware|
|
265
|
+
current_chain = chain
|
266
|
+
chain = lambda do
|
267
|
+
if middleware.respond_to?(:call)
|
268
|
+
middleware.call(request, context) { current_chain.call }
|
269
|
+
else
|
270
|
+
# Fallback for middleware that don't implement call
|
271
|
+
current_chain.call
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
# Execute the chain
|
277
|
+
chain.call
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|