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
data/docs/server_apps.md
ADDED
@@ -0,0 +1,621 @@
|
|
1
|
+
# Server Applications
|
2
|
+
|
3
|
+
The A2A Ruby SDK provides ready-to-use server applications that can be deployed with popular Ruby web frameworks. These applications handle all the A2A protocol details, allowing you to focus on your agent logic.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
The SDK includes two main server application types:
|
8
|
+
|
9
|
+
- **RackApp**: A Rack-compatible application that works with any Rack server
|
10
|
+
- **SinatraApp**: A Sinatra-based application with Ruby-idiomatic routing
|
11
|
+
|
12
|
+
Both applications provide:
|
13
|
+
- Agent card serving at `/.well-known/a2a/agent-card`
|
14
|
+
- Extended agent card serving at `/a2a/agent-card/extended`
|
15
|
+
- JSON-RPC endpoint at `/a2a/rpc`
|
16
|
+
- Server-Sent Events for streaming responses
|
17
|
+
- Proper error handling and response formatting
|
18
|
+
|
19
|
+
## Rack Application
|
20
|
+
|
21
|
+
### Basic Setup
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
require 'a2a'
|
25
|
+
|
26
|
+
# Create agent card
|
27
|
+
agent_card = A2A::Types::AgentCard.new(
|
28
|
+
name: "My Agent",
|
29
|
+
description: "A helpful agent",
|
30
|
+
version: "1.0.0",
|
31
|
+
capabilities: A2A::Types::Capabilities.new(
|
32
|
+
streaming: true,
|
33
|
+
push_notifications: true,
|
34
|
+
task_management: true
|
35
|
+
)
|
36
|
+
)
|
37
|
+
|
38
|
+
# Create agent executor
|
39
|
+
class MyAgentExecutor < A2A::Server::AgentExecution::SimpleAgentExecutor
|
40
|
+
def process_message(message, task, context)
|
41
|
+
text = message.parts.first.text
|
42
|
+
{ response: "You said: #{text}" }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Create request handler
|
47
|
+
executor = MyAgentExecutor.new
|
48
|
+
handler = A2A::Server::DefaultRequestHandler.new(executor)
|
49
|
+
|
50
|
+
# Create Rack application
|
51
|
+
app = A2A::Server::Apps::RackApp.new(
|
52
|
+
agent_card: agent_card,
|
53
|
+
request_handler: handler
|
54
|
+
)
|
55
|
+
|
56
|
+
# Run with any Rack server
|
57
|
+
# rackup -p 9292
|
58
|
+
```
|
59
|
+
|
60
|
+
### Advanced Configuration
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
app = A2A::Server::Apps::RackApp.new(
|
64
|
+
agent_card: agent_card,
|
65
|
+
request_handler: handler,
|
66
|
+
extended_agent_card: extended_card, # Optional extended card
|
67
|
+
card_modifier: ->(card) {
|
68
|
+
# Dynamically modify the public card
|
69
|
+
card.metadata ||= {}
|
70
|
+
card.metadata[:server_time] = Time.now.utc.iso8601
|
71
|
+
card.metadata[:ruby_version] = RUBY_VERSION
|
72
|
+
card
|
73
|
+
},
|
74
|
+
extended_card_modifier: ->(card, context) {
|
75
|
+
# Modify extended card based on context
|
76
|
+
if context.authenticated?
|
77
|
+
card.metadata ||= {}
|
78
|
+
card.metadata[:user] = context.user.to_s
|
79
|
+
end
|
80
|
+
card
|
81
|
+
}
|
82
|
+
)
|
83
|
+
```
|
84
|
+
|
85
|
+
### Deployment with config.ru
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
# config.ru
|
89
|
+
require_relative 'my_agent'
|
90
|
+
|
91
|
+
# Add middleware
|
92
|
+
use Rack::Logger
|
93
|
+
use Rack::CommonLogger
|
94
|
+
|
95
|
+
# CORS for development
|
96
|
+
if ENV['RACK_ENV'] == 'development'
|
97
|
+
use Rack::Cors do
|
98
|
+
allow do
|
99
|
+
origins '*'
|
100
|
+
resource '*',
|
101
|
+
headers: :any,
|
102
|
+
methods: [:get, :post, :options]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Authentication middleware (optional)
|
108
|
+
use MyAuthMiddleware
|
109
|
+
|
110
|
+
# Run the A2A app
|
111
|
+
run create_a2a_app
|
112
|
+
```
|
113
|
+
|
114
|
+
### Custom Middleware Integration
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
class AuthenticationMiddleware
|
118
|
+
def initialize(app)
|
119
|
+
@app = app
|
120
|
+
end
|
121
|
+
|
122
|
+
def call(env)
|
123
|
+
# Extract authentication from headers
|
124
|
+
auth_header = env['HTTP_AUTHORIZATION']
|
125
|
+
|
126
|
+
if auth_header&.start_with?('Bearer ')
|
127
|
+
token = auth_header[7..-1]
|
128
|
+
user = authenticate_token(token)
|
129
|
+
env['current_user'] = user if user
|
130
|
+
end
|
131
|
+
|
132
|
+
@app.call(env)
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def authenticate_token(token)
|
138
|
+
# Your authentication logic
|
139
|
+
User.find_by_token(token)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Use with Rack app
|
144
|
+
use AuthenticationMiddleware
|
145
|
+
run app
|
146
|
+
```
|
147
|
+
|
148
|
+
## Sinatra Application
|
149
|
+
|
150
|
+
### Basic Setup
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
require 'sinatra'
|
154
|
+
require 'a2a'
|
155
|
+
|
156
|
+
class MyA2AApp < A2A::Server::Apps::SinatraApp
|
157
|
+
# Configure A2A components
|
158
|
+
configure_a2a(
|
159
|
+
agent_card: agent_card,
|
160
|
+
request_handler: handler
|
161
|
+
)
|
162
|
+
|
163
|
+
# Add custom routes if needed
|
164
|
+
get '/health' do
|
165
|
+
{ status: 'ok', timestamp: Time.now.utc.iso8601 }.to_json
|
166
|
+
end
|
167
|
+
|
168
|
+
# Custom error handling
|
169
|
+
error A2A::Errors::AuthenticationRequired do
|
170
|
+
status 401
|
171
|
+
{ error: 'Authentication required' }.to_json
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Run the app
|
176
|
+
MyA2AApp.run! port: 9292
|
177
|
+
```
|
178
|
+
|
179
|
+
### Advanced Sinatra Integration
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
class AdvancedA2AApp < A2A::Server::Apps::SinatraApp
|
183
|
+
# Enable sessions for authentication
|
184
|
+
enable :sessions
|
185
|
+
|
186
|
+
# Configure A2A with dynamic card modification
|
187
|
+
configure_a2a(
|
188
|
+
agent_card: agent_card,
|
189
|
+
request_handler: handler,
|
190
|
+
card_modifier: method(:modify_public_card),
|
191
|
+
extended_card_modifier: method(:modify_extended_card)
|
192
|
+
)
|
193
|
+
|
194
|
+
# Authentication helpers
|
195
|
+
helpers do
|
196
|
+
def current_user
|
197
|
+
@current_user ||= User.find(session[:user_id]) if session[:user_id]
|
198
|
+
end
|
199
|
+
|
200
|
+
def authenticated?
|
201
|
+
!current_user.nil?
|
202
|
+
end
|
203
|
+
|
204
|
+
def require_auth!
|
205
|
+
halt 401, { error: 'Authentication required' }.to_json unless authenticated?
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Authentication routes
|
210
|
+
post '/auth/login' do
|
211
|
+
user = User.authenticate(params[:username], params[:password])
|
212
|
+
if user
|
213
|
+
session[:user_id] = user.id
|
214
|
+
{ success: true, user: user.to_h }.to_json
|
215
|
+
else
|
216
|
+
status 401
|
217
|
+
{ error: 'Invalid credentials' }.to_json
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
post '/auth/logout' do
|
222
|
+
session.clear
|
223
|
+
{ success: true }.to_json
|
224
|
+
end
|
225
|
+
|
226
|
+
# Protected routes
|
227
|
+
get '/admin/stats' do
|
228
|
+
require_auth!
|
229
|
+
content_type :json
|
230
|
+
|
231
|
+
{
|
232
|
+
tasks_processed: TaskStats.total_processed,
|
233
|
+
active_sessions: SessionManager.active_count,
|
234
|
+
uptime: Time.now - start_time
|
235
|
+
}.to_json
|
236
|
+
end
|
237
|
+
|
238
|
+
private
|
239
|
+
|
240
|
+
def modify_public_card(card)
|
241
|
+
card.metadata ||= {}
|
242
|
+
card.metadata[:server_info] = {
|
243
|
+
ruby_version: RUBY_VERSION,
|
244
|
+
sinatra_version: Sinatra::VERSION,
|
245
|
+
uptime: Time.now - start_time
|
246
|
+
}
|
247
|
+
card
|
248
|
+
end
|
249
|
+
|
250
|
+
def modify_extended_card(card, context)
|
251
|
+
if context.authenticated?
|
252
|
+
card.metadata ||= {}
|
253
|
+
card.metadata[:user_info] = {
|
254
|
+
id: context.user.id,
|
255
|
+
name: context.user.name,
|
256
|
+
permissions: context.user.permissions
|
257
|
+
}
|
258
|
+
end
|
259
|
+
card
|
260
|
+
end
|
261
|
+
end
|
262
|
+
```
|
263
|
+
|
264
|
+
## Rails Integration
|
265
|
+
|
266
|
+
### Engine Mount
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
# config/routes.rb
|
270
|
+
Rails.application.routes.draw do
|
271
|
+
mount A2A::Engine => "/a2a"
|
272
|
+
|
273
|
+
# Or mount custom A2A app
|
274
|
+
mount MyA2AApp => "/custom-a2a"
|
275
|
+
end
|
276
|
+
```
|
277
|
+
|
278
|
+
### Custom Rails Controller
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
class A2AController < ApplicationController
|
282
|
+
before_action :authenticate_user!, only: [:extended_card]
|
283
|
+
|
284
|
+
def rpc
|
285
|
+
# Create request handler for this request
|
286
|
+
handler = create_request_handler
|
287
|
+
|
288
|
+
# Parse JSON-RPC request
|
289
|
+
rpc_request = A2A::Protocol::JsonRpc.parse_request(request.body.read)
|
290
|
+
|
291
|
+
# Route to handler
|
292
|
+
result = route_to_handler(rpc_request, handler)
|
293
|
+
|
294
|
+
# Return response
|
295
|
+
if result.is_a?(Enumerator)
|
296
|
+
# Handle streaming
|
297
|
+
render_streaming_response(result)
|
298
|
+
else
|
299
|
+
render json: A2A::Protocol::JsonRpc.build_response(
|
300
|
+
result: result,
|
301
|
+
id: rpc_request.id
|
302
|
+
)
|
303
|
+
end
|
304
|
+
rescue A2A::Errors::A2AError => e
|
305
|
+
render json: A2A::Protocol::JsonRpc.build_error_response(
|
306
|
+
code: e.code,
|
307
|
+
message: e.message,
|
308
|
+
id: rpc_request&.id
|
309
|
+
)
|
310
|
+
end
|
311
|
+
|
312
|
+
def agent_card
|
313
|
+
card = build_agent_card
|
314
|
+
|
315
|
+
# Apply modifications
|
316
|
+
card = modify_card_for_user(card, current_user) if respond_to?(:current_user)
|
317
|
+
|
318
|
+
render json: card.to_h
|
319
|
+
end
|
320
|
+
|
321
|
+
def extended_agent_card
|
322
|
+
card = build_extended_agent_card
|
323
|
+
card = modify_extended_card_for_user(card, current_user)
|
324
|
+
|
325
|
+
render json: card.to_h
|
326
|
+
end
|
327
|
+
|
328
|
+
private
|
329
|
+
|
330
|
+
def create_request_handler
|
331
|
+
executor = MyAgentExecutor.new
|
332
|
+
A2A::Server::DefaultRequestHandler.new(executor)
|
333
|
+
end
|
334
|
+
|
335
|
+
def render_streaming_response(enumerator)
|
336
|
+
response.headers['Content-Type'] = 'text/event-stream'
|
337
|
+
response.headers['Cache-Control'] = 'no-cache'
|
338
|
+
response.headers['Connection'] = 'keep-alive'
|
339
|
+
|
340
|
+
self.response_body = Enumerator.new do |yielder|
|
341
|
+
enumerator.each do |event|
|
342
|
+
data = event.respond_to?(:to_h) ? event.to_h : event
|
343
|
+
yielder << "data: #{data.to_json}\n\n"
|
344
|
+
end
|
345
|
+
rescue => e
|
346
|
+
yielder << "data: #{JSON.generate(error: e.message)}\n\n"
|
347
|
+
ensure
|
348
|
+
yielder << "data: [DONE]\n\n"
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
```
|
353
|
+
|
354
|
+
## Deployment Options
|
355
|
+
|
356
|
+
### Puma (Recommended)
|
357
|
+
|
358
|
+
```ruby
|
359
|
+
# config/puma.rb
|
360
|
+
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
|
361
|
+
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
|
362
|
+
threads threads_count, threads_count
|
363
|
+
|
364
|
+
preload_app!
|
365
|
+
|
366
|
+
rackup DefaultRackup
|
367
|
+
port ENV.fetch("PORT") { 3000 }
|
368
|
+
environment ENV.fetch("RACK_ENV") { "development" }
|
369
|
+
|
370
|
+
on_worker_boot do
|
371
|
+
# Worker-specific initialization
|
372
|
+
A2A.initialize_monitoring!
|
373
|
+
end
|
374
|
+
```
|
375
|
+
|
376
|
+
### Unicorn
|
377
|
+
|
378
|
+
```ruby
|
379
|
+
# config/unicorn.rb
|
380
|
+
worker_processes Integer(ENV["WEB_CONCURRENCY"] || 3)
|
381
|
+
timeout 30
|
382
|
+
preload_app true
|
383
|
+
|
384
|
+
before_fork do |server, worker|
|
385
|
+
# Close database connections
|
386
|
+
ActiveRecord::Base.connection.disconnect! if defined?(ActiveRecord::Base)
|
387
|
+
end
|
388
|
+
|
389
|
+
after_fork do |server, worker|
|
390
|
+
# Reconnect database
|
391
|
+
ActiveRecord::Base.establish_connection if defined?(ActiveRecord::Base)
|
392
|
+
|
393
|
+
# Initialize A2A monitoring
|
394
|
+
A2A.initialize_monitoring!
|
395
|
+
end
|
396
|
+
```
|
397
|
+
|
398
|
+
### Docker Deployment
|
399
|
+
|
400
|
+
```dockerfile
|
401
|
+
FROM ruby:3.2-alpine
|
402
|
+
|
403
|
+
WORKDIR /app
|
404
|
+
|
405
|
+
# Install dependencies
|
406
|
+
COPY Gemfile Gemfile.lock ./
|
407
|
+
RUN bundle install --deployment --without development test
|
408
|
+
|
409
|
+
# Copy application
|
410
|
+
COPY . .
|
411
|
+
|
412
|
+
# Expose port
|
413
|
+
EXPOSE 9292
|
414
|
+
|
415
|
+
# Run with Puma
|
416
|
+
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
|
417
|
+
```
|
418
|
+
|
419
|
+
```yaml
|
420
|
+
# docker-compose.yml
|
421
|
+
version: '3.8'
|
422
|
+
services:
|
423
|
+
a2a-agent:
|
424
|
+
build: .
|
425
|
+
ports:
|
426
|
+
- "9292:9292"
|
427
|
+
environment:
|
428
|
+
- RACK_ENV=production
|
429
|
+
- A2A_LOG_LEVEL=info
|
430
|
+
volumes:
|
431
|
+
- ./logs:/app/logs
|
432
|
+
depends_on:
|
433
|
+
- redis
|
434
|
+
- postgres
|
435
|
+
|
436
|
+
redis:
|
437
|
+
image: redis:alpine
|
438
|
+
|
439
|
+
postgres:
|
440
|
+
image: postgres:13
|
441
|
+
environment:
|
442
|
+
POSTGRES_DB: a2a_production
|
443
|
+
POSTGRES_USER: a2a
|
444
|
+
POSTGRES_PASSWORD: secret
|
445
|
+
```
|
446
|
+
|
447
|
+
## Monitoring and Health Checks
|
448
|
+
|
449
|
+
### Health Check Endpoint
|
450
|
+
|
451
|
+
```ruby
|
452
|
+
class HealthCheckApp
|
453
|
+
def call(env)
|
454
|
+
if env['PATH_INFO'] == '/health'
|
455
|
+
status = check_health
|
456
|
+
|
457
|
+
if status[:healthy]
|
458
|
+
[200, {'Content-Type' => 'application/json'}, [status.to_json]]
|
459
|
+
else
|
460
|
+
[503, {'Content-Type' => 'application/json'}, [status.to_json]]
|
461
|
+
end
|
462
|
+
else
|
463
|
+
[404, {}, ['Not Found']]
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
private
|
468
|
+
|
469
|
+
def check_health
|
470
|
+
{
|
471
|
+
healthy: true,
|
472
|
+
timestamp: Time.now.utc.iso8601,
|
473
|
+
version: A2A::VERSION,
|
474
|
+
checks: {
|
475
|
+
database: check_database,
|
476
|
+
redis: check_redis,
|
477
|
+
memory: check_memory
|
478
|
+
}
|
479
|
+
}
|
480
|
+
rescue => e
|
481
|
+
{
|
482
|
+
healthy: false,
|
483
|
+
error: e.message,
|
484
|
+
timestamp: Time.now.utc.iso8601
|
485
|
+
}
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
# Mount health check
|
490
|
+
map '/health' do
|
491
|
+
run HealthCheckApp.new
|
492
|
+
end
|
493
|
+
|
494
|
+
map '/' do
|
495
|
+
run a2a_app
|
496
|
+
end
|
497
|
+
```
|
498
|
+
|
499
|
+
### Metrics Collection
|
500
|
+
|
501
|
+
```ruby
|
502
|
+
class MetricsMiddleware
|
503
|
+
def initialize(app)
|
504
|
+
@app = app
|
505
|
+
end
|
506
|
+
|
507
|
+
def call(env)
|
508
|
+
start_time = Time.now
|
509
|
+
|
510
|
+
status, headers, body = @app.call(env)
|
511
|
+
|
512
|
+
duration = Time.now - start_time
|
513
|
+
|
514
|
+
# Record metrics
|
515
|
+
A2A.record_metric('http_requests_total', 1,
|
516
|
+
method: env['REQUEST_METHOD'],
|
517
|
+
path: env['PATH_INFO'],
|
518
|
+
status: status
|
519
|
+
)
|
520
|
+
|
521
|
+
A2A.record_metric('http_request_duration_seconds', duration,
|
522
|
+
method: env['REQUEST_METHOD'],
|
523
|
+
path: env['PATH_INFO']
|
524
|
+
)
|
525
|
+
|
526
|
+
[status, headers, body]
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
use MetricsMiddleware
|
531
|
+
```
|
532
|
+
|
533
|
+
## Testing Server Applications
|
534
|
+
|
535
|
+
### Testing Rack App
|
536
|
+
|
537
|
+
```ruby
|
538
|
+
RSpec.describe A2A::Server::Apps::RackApp do
|
539
|
+
include Rack::Test::Methods
|
540
|
+
|
541
|
+
let(:app) { create_test_app }
|
542
|
+
|
543
|
+
describe "GET /.well-known/a2a/agent-card" do
|
544
|
+
it "returns agent card" do
|
545
|
+
get "/.well-known/a2a/agent-card"
|
546
|
+
|
547
|
+
expect(last_response).to be_ok
|
548
|
+
expect(last_response.content_type).to include('application/json')
|
549
|
+
|
550
|
+
card = JSON.parse(last_response.body)
|
551
|
+
expect(card['name']).to eq('Test Agent')
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
describe "POST /a2a/rpc" do
|
556
|
+
it "handles JSON-RPC requests" do
|
557
|
+
request = {
|
558
|
+
jsonrpc: "2.0",
|
559
|
+
method: "message/send",
|
560
|
+
params: { message: test_message.to_h },
|
561
|
+
id: 1
|
562
|
+
}
|
563
|
+
|
564
|
+
post "/a2a/rpc", request.to_json,
|
565
|
+
'CONTENT_TYPE' => 'application/json'
|
566
|
+
|
567
|
+
expect(last_response).to be_ok
|
568
|
+
|
569
|
+
response = JSON.parse(last_response.body)
|
570
|
+
expect(response['jsonrpc']).to eq('2.0')
|
571
|
+
expect(response['id']).to eq(1)
|
572
|
+
end
|
573
|
+
end
|
574
|
+
end
|
575
|
+
```
|
576
|
+
|
577
|
+
### Testing Sinatra App
|
578
|
+
|
579
|
+
```ruby
|
580
|
+
RSpec.describe MyA2AApp do
|
581
|
+
include Rack::Test::Methods
|
582
|
+
|
583
|
+
let(:app) { MyA2AApp }
|
584
|
+
|
585
|
+
it "handles authentication" do
|
586
|
+
post "/auth/login", {
|
587
|
+
username: "test",
|
588
|
+
password: "password"
|
589
|
+
}
|
590
|
+
|
591
|
+
expect(last_response).to be_ok
|
592
|
+
|
593
|
+
# Test authenticated request
|
594
|
+
get "/admin/stats"
|
595
|
+
expect(last_response).to be_ok
|
596
|
+
end
|
597
|
+
end
|
598
|
+
```
|
599
|
+
|
600
|
+
## Best Practices
|
601
|
+
|
602
|
+
1. **Security**: Always implement proper authentication and authorization
|
603
|
+
2. **Error Handling**: Provide meaningful error responses
|
604
|
+
3. **Logging**: Log important events and errors
|
605
|
+
4. **Monitoring**: Monitor application health and performance
|
606
|
+
5. **Testing**: Write comprehensive tests for your endpoints
|
607
|
+
6. **Documentation**: Document your API endpoints and authentication requirements
|
608
|
+
7. **Deployment**: Use proper deployment practices with process managers
|
609
|
+
8. **Scaling**: Consider horizontal scaling for high-traffic scenarios
|
610
|
+
|
611
|
+
The server applications provide a solid foundation for deploying A2A agents in production environments while maintaining flexibility for customization and integration with existing systems.
|
612
|
+
|
613
|
+
## Complete Examples
|
614
|
+
|
615
|
+
For complete working examples of server applications, see the [A2A Ruby Samples Repository](https://github.com/a2aproject/a2a-ruby-samples), which includes:
|
616
|
+
|
617
|
+
- **Rack Applications** - Production-ready Rack server examples
|
618
|
+
- **Sinatra Integration** - Lightweight web service examples
|
619
|
+
- **Rails Applications** - Full Rails integration with web UI
|
620
|
+
- **Docker Deployment** - Container-based deployment examples
|
621
|
+
- **Multi-Agent Systems** - Complex agent orchestration examples
|