Class: A2A::Server::Apps::RackApp

Inherits:
Object
  • Object
show all
Defined in:
lib/a2a/server/apps/rack_app.rb

Overview

Rack application for serving A2A protocol endpoints

This class provides a Rack-compatible application that can handle A2A JSON-RPC requests and serve agent cards. It's similar to the Python FastAPI implementation but uses Rack for Ruby web servers.

Constant Summary collapse

AGENT_CARD_PATH =
"/.well-known/a2a/agent-card"
EXTENDED_AGENT_CARD_PATH =
"/a2a/agent-card/extended"
RPC_PATH =
"/a2a/rpc"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(agent_card:, request_handler:, extended_agent_card: nil, card_modifier: nil, extended_card_modifier: nil) ⇒ RackApp

Initialize the Rack application

Parameters:

  • The agent card describing capabilities

  • The request handler for processing A2A requests

  • (defaults to: nil)

    Optional extended agent card

  • (defaults to: nil)

    Optional callback to modify the public agent card

  • (defaults to: nil)

    Optional callback to modify the extended agent card



33
34
35
36
37
38
39
# File 'lib/a2a/server/apps/rack_app.rb', line 33

def initialize(agent_card:, request_handler:, extended_agent_card: nil, card_modifier: nil, extended_card_modifier: nil)
  @agent_card = agent_card
  @request_handler = request_handler
  @extended_agent_card = extended_agent_card
  @card_modifier = card_modifier
  @extended_card_modifier = extended_card_modifier
end

Instance Attribute Details

#agent_cardObject (readonly)

Returns the value of attribute agent_card.



23
24
25
# File 'lib/a2a/server/apps/rack_app.rb', line 23

def agent_card
  @agent_card
end

#extended_agent_cardObject (readonly)

Returns the value of attribute extended_agent_card.



23
24
25
# File 'lib/a2a/server/apps/rack_app.rb', line 23

def extended_agent_card
  @extended_agent_card
end

#request_handlerObject (readonly)

Returns the value of attribute request_handler.



23
24
25
# File 'lib/a2a/server/apps/rack_app.rb', line 23

def request_handler
  @request_handler
end

Instance Method Details

#build_server_context(request) ⇒ A2A::Server::Context (private)

Build server context from Rack request

Parameters:

  • The Rack request

Returns:

  • The server context



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/a2a/server/apps/rack_app.rb', line 190

def build_server_context(request)
  context = A2A::Server::Context.new

  # Extract user information if available (depends on authentication middleware)
  if request.env["warden"]&.authenticated?
    context.set_user(request.env["warden"].user)
    context.set_authentication("warden", request.env["warden"])
  elsif request.env["current_user"]
    context.set_user(request.env["current_user"])
  end

  # Set request metadata
  context.(:remote_addr, request.ip)
  context.(:user_agent, request.user_agent)
  context.(:headers, request.env.select { |k, _| k.start_with?("HTTP_") })

  context
end

#call(env) ⇒ Array

Rack application call method

Parameters:

  • Rack environment

Returns:

  • Rack response [status, headers, body]



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/a2a/server/apps/rack_app.rb', line 46

def call(env)
  request = Rack::Request.new(env)

  case request.path_info
  when AGENT_CARD_PATH
    handle_agent_card(request)
  when EXTENDED_AGENT_CARD_PATH
    handle_extended_agent_card(request)
  when RPC_PATH
    handle_rpc_request(request)
  else
    not_found_response
  end
rescue StandardError => e
  error_response(500, "Internal Server Error: #{e.message}")
end

#error_response(status, message) ⇒ Array (private)

Create an error response

Parameters:

  • HTTP status code

  • Error message

Returns:

  • Rack response



303
304
305
306
307
# File 'lib/a2a/server/apps/rack_app.rb', line 303

def error_response(status, message)
  headers = { "Content-Type" => "application/json" }
  body = JSON.generate({ error: message })
  [status, headers, [body]]
end

#handle_agent_card(request) ⇒ Array (private)

Handle agent card requests

Parameters:

  • The request object

Returns:

  • Rack response



70
71
72
73
74
75
76
77
# File 'lib/a2a/server/apps/rack_app.rb', line 70

def handle_agent_card(request)
  return method_not_allowed_response unless request.get?

  card_to_serve = @agent_card
  card_to_serve = @card_modifier.call(card_to_serve) if @card_modifier

  json_response(200, card_to_serve.to_h)
end

#handle_extended_agent_card(request) ⇒ Array (private)

Handle extended agent card requests

Parameters:

  • The request object

Returns:

  • Rack response



84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/a2a/server/apps/rack_app.rb', line 84

def handle_extended_agent_card(request)
  return method_not_allowed_response unless request.get?

  return error_response(404, "Extended agent card not supported") unless @agent_card.supports_authenticated_extended_card

  # Build server context from request
  context = build_server_context(request)

  card_to_serve = @extended_agent_card || @agent_card

  card_to_serve = @extended_card_modifier.call(card_to_serve, context) if @extended_card_modifier

  json_response(200, card_to_serve.to_h)
end

#handle_rpc_request(request) ⇒ Array (private)

Handle JSON-RPC requests

Parameters:

  • The request object

Returns:

  • Rack response



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/a2a/server/apps/rack_app.rb', line 104

def handle_rpc_request(request)
  return method_not_allowed_response unless request.post?

  # Check content type
  content_type = request.content_type
  return error_response(400, "Content-Type must be application/json") unless content_type&.include?("application/json")

  # Parse request body
  body = request.body.read
  request.body.rewind

  # Parse JSON-RPC request directly from string
  begin
    rpc_request = A2A::Protocol::JsonRpc.parse_request(body)
  rescue A2A::Errors::A2AError => e
    return json_rpc_error_response(
      nil, # No ID available if parsing failed
      e.code,
      e.message,
      e.data
    )
  end

  # Build server context
  context = build_server_context(request)

  # Route to appropriate handler method
  begin
    result = route_request(rpc_request, context)

    # Handle streaming responses
    return streaming_response(result) if result.is_a?(Enumerator)

    # Return regular JSON-RPC response
    response_data = A2A::Protocol::JsonRpc.build_response(
      result: result,
      id: rpc_request.id
    )
    json_response(200, response_data)
  rescue A2A::Errors::A2AError => e
    json_rpc_error_response(rpc_request.id, e.code, e.message, e.data)
  rescue StandardError => e
    json_rpc_error_response(
      rpc_request.id,
      A2A::Protocol::JsonRpc::INTERNAL_ERROR,
      "Internal error: #{e.message}"
    )
  end
end

#json_response(status, data) ⇒ Array (private)

Create a JSON response

Parameters:

  • HTTP status code

  • Data to serialize as JSON

Returns:

  • Rack response



215
216
217
218
219
220
221
222
223
# File 'lib/a2a/server/apps/rack_app.rb', line 215

def json_response(status, data)
  headers = {
    "Content-Type" => "application/json",
    "Cache-Control" => "no-cache"
  }

  body = JSON.generate(data)
  [status, headers, [body]]
end

#json_rpc_error_response(id, code, message, data = nil) ⇒ Array (private)

Create a JSON-RPC error response

Parameters:

  • Request ID

  • Error code

  • Error message

  • (defaults to: nil)

    Optional error data

Returns:

  • Rack response



233
234
235
236
237
238
239
240
241
# File 'lib/a2a/server/apps/rack_app.rb', line 233

def json_rpc_error_response(id, code, message, data = nil)
  error_data = A2A::Protocol::JsonRpc.build_error_response(
    code: code,
    message: message,
    data: data,
    id: id
  )
  json_response(200, error_data)
end

#method_not_allowed_responseArray (private)

Create a 405 Method Not Allowed response

Returns:

  • Rack response



293
294
295
# File 'lib/a2a/server/apps/rack_app.rb', line 293

def method_not_allowed_response
  error_response(405, "Method Not Allowed")
end

#not_found_responseArray (private)

Create a 404 Not Found response

Returns:

  • Rack response



285
286
287
# File 'lib/a2a/server/apps/rack_app.rb', line 285

def not_found_response
  error_response(404, "Not Found")
end

#route_request(request, context) ⇒ Object (private)

Route JSON-RPC request to appropriate handler method

Parameters:

  • The parsed JSON-RPC request

  • The server context

Returns:

  • The result from the handler



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/a2a/server/apps/rack_app.rb', line 160

def route_request(request, context)
  case request.method
  when "message/send"
    @request_handler.on_message_send(request.params, context)
  when "message/stream"
    @request_handler.on_message_send_stream(request.params, context)
  when "tasks/get"
    @request_handler.on_get_task(request.params, context)
  when "tasks/cancel"
    @request_handler.on_cancel_task(request.params, context)
  when "tasks/resubscribe"
    @request_handler.on_resubscribe_to_task(request.params, context)
  when "tasks/pushNotificationConfig/set"
    @request_handler.on_set_task_push_notification_config(request.params, context)
  when "tasks/pushNotificationConfig/get"
    @request_handler.on_get_task_push_notification_config(request.params, context)
  when "tasks/pushNotificationConfig/list"
    @request_handler.on_list_task_push_notification_config(request.params, context)
  when "tasks/pushNotificationConfig/delete"
    @request_handler.on_delete_task_push_notification_config(request.params, context)
  else
    raise A2A::Errors::MethodNotFound, "Method '#{request.method}' not found"
  end
end

#streaming_response(enumerator) ⇒ Array (private)

Create a streaming response using Server-Sent Events

Parameters:

  • The enumerator yielding events

Returns:

  • Rack response



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/a2a/server/apps/rack_app.rb', line 248

def streaming_response(enumerator)
  headers = {
    "Content-Type" => "text/event-stream",
    "Cache-Control" => "no-cache",
    "Connection" => "keep-alive"
  }

  # Create streaming body
  body = Enumerator.new do |yielder|
    enumerator.each do |event|
      event_data = if event.respond_to?(:to_h)
                     event.to_h
                   else
                     event
                   end

      yielder << "data: #{JSON.generate(event_data)}\n\n"
    end
  rescue StandardError => e
    error_event = {
      error: {
        code: A2A::Protocol::JsonRpc::INTERNAL_ERROR,
        message: e.message
      }
    }
    yielder << "data: #{JSON.generate(error_event)}\n\n"
  ensure
    yielder << "data: [DONE]\n\n"
  end

  [200, headers, body]
end