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.
Files changed (128) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +137 -0
  4. data/.simplecov +46 -0
  5. data/.yardopts +10 -0
  6. data/CHANGELOG.md +33 -0
  7. data/CODE_OF_CONDUCT.md +128 -0
  8. data/CONTRIBUTING.md +165 -0
  9. data/Gemfile +43 -0
  10. data/Guardfile +34 -0
  11. data/LICENSE.txt +21 -0
  12. data/PUBLISHING_CHECKLIST.md +214 -0
  13. data/README.md +171 -0
  14. data/Rakefile +165 -0
  15. data/docs/agent_execution.md +309 -0
  16. data/docs/api_reference.md +792 -0
  17. data/docs/configuration.md +780 -0
  18. data/docs/events.md +475 -0
  19. data/docs/getting_started.md +668 -0
  20. data/docs/integration.md +262 -0
  21. data/docs/server_apps.md +621 -0
  22. data/docs/troubleshooting.md +765 -0
  23. data/lib/a2a/client/api_methods.rb +263 -0
  24. data/lib/a2a/client/auth/api_key.rb +161 -0
  25. data/lib/a2a/client/auth/interceptor.rb +288 -0
  26. data/lib/a2a/client/auth/jwt.rb +189 -0
  27. data/lib/a2a/client/auth/oauth2.rb +146 -0
  28. data/lib/a2a/client/auth.rb +137 -0
  29. data/lib/a2a/client/base.rb +316 -0
  30. data/lib/a2a/client/config.rb +210 -0
  31. data/lib/a2a/client/connection_pool.rb +233 -0
  32. data/lib/a2a/client/http_client.rb +524 -0
  33. data/lib/a2a/client/json_rpc_handler.rb +136 -0
  34. data/lib/a2a/client/middleware/circuit_breaker_interceptor.rb +245 -0
  35. data/lib/a2a/client/middleware/logging_interceptor.rb +371 -0
  36. data/lib/a2a/client/middleware/rate_limit_interceptor.rb +142 -0
  37. data/lib/a2a/client/middleware/retry_interceptor.rb +161 -0
  38. data/lib/a2a/client/middleware.rb +116 -0
  39. data/lib/a2a/client/performance_tracker.rb +60 -0
  40. data/lib/a2a/configuration/defaults.rb +34 -0
  41. data/lib/a2a/configuration/environment_loader.rb +76 -0
  42. data/lib/a2a/configuration/file_loader.rb +115 -0
  43. data/lib/a2a/configuration/inheritance.rb +101 -0
  44. data/lib/a2a/configuration/validator.rb +180 -0
  45. data/lib/a2a/configuration.rb +201 -0
  46. data/lib/a2a/errors.rb +291 -0
  47. data/lib/a2a/modules.rb +50 -0
  48. data/lib/a2a/monitoring/alerting.rb +490 -0
  49. data/lib/a2a/monitoring/distributed_tracing.rb +398 -0
  50. data/lib/a2a/monitoring/health_endpoints.rb +204 -0
  51. data/lib/a2a/monitoring/metrics_collector.rb +438 -0
  52. data/lib/a2a/monitoring.rb +463 -0
  53. data/lib/a2a/plugin.rb +358 -0
  54. data/lib/a2a/plugin_manager.rb +159 -0
  55. data/lib/a2a/plugins/example_auth.rb +81 -0
  56. data/lib/a2a/plugins/example_middleware.rb +118 -0
  57. data/lib/a2a/plugins/example_transport.rb +76 -0
  58. data/lib/a2a/protocol/agent_card.rb +8 -0
  59. data/lib/a2a/protocol/agent_card_server.rb +584 -0
  60. data/lib/a2a/protocol/capability.rb +496 -0
  61. data/lib/a2a/protocol/json_rpc.rb +254 -0
  62. data/lib/a2a/protocol/message.rb +8 -0
  63. data/lib/a2a/protocol/task.rb +8 -0
  64. data/lib/a2a/rails/a2a_controller.rb +258 -0
  65. data/lib/a2a/rails/controller_helpers.rb +499 -0
  66. data/lib/a2a/rails/engine.rb +167 -0
  67. data/lib/a2a/rails/generators/agent_generator.rb +311 -0
  68. data/lib/a2a/rails/generators/install_generator.rb +209 -0
  69. data/lib/a2a/rails/generators/migration_generator.rb +232 -0
  70. data/lib/a2a/rails/generators/templates/add_a2a_indexes.rb +57 -0
  71. data/lib/a2a/rails/generators/templates/agent_controller.rb +122 -0
  72. data/lib/a2a/rails/generators/templates/agent_controller_spec.rb +160 -0
  73. data/lib/a2a/rails/generators/templates/agent_readme.md +200 -0
  74. data/lib/a2a/rails/generators/templates/create_a2a_push_notification_configs.rb +68 -0
  75. data/lib/a2a/rails/generators/templates/create_a2a_tasks.rb +83 -0
  76. data/lib/a2a/rails/generators/templates/example_agent_controller.rb +228 -0
  77. data/lib/a2a/rails/generators/templates/initializer.rb +108 -0
  78. data/lib/a2a/rails/generators/templates/push_notification_config_model.rb +228 -0
  79. data/lib/a2a/rails/generators/templates/task_model.rb +200 -0
  80. data/lib/a2a/rails/tasks/a2a.rake +228 -0
  81. data/lib/a2a/server/a2a_methods.rb +520 -0
  82. data/lib/a2a/server/agent.rb +537 -0
  83. data/lib/a2a/server/agent_execution/agent_executor.rb +279 -0
  84. data/lib/a2a/server/agent_execution/request_context.rb +219 -0
  85. data/lib/a2a/server/apps/rack_app.rb +311 -0
  86. data/lib/a2a/server/apps/sinatra_app.rb +261 -0
  87. data/lib/a2a/server/default_request_handler.rb +350 -0
  88. data/lib/a2a/server/events/event_consumer.rb +116 -0
  89. data/lib/a2a/server/events/event_queue.rb +226 -0
  90. data/lib/a2a/server/example_agent.rb +248 -0
  91. data/lib/a2a/server/handler.rb +281 -0
  92. data/lib/a2a/server/middleware/authentication_middleware.rb +212 -0
  93. data/lib/a2a/server/middleware/cors_middleware.rb +171 -0
  94. data/lib/a2a/server/middleware/logging_middleware.rb +362 -0
  95. data/lib/a2a/server/middleware/rate_limit_middleware.rb +382 -0
  96. data/lib/a2a/server/middleware.rb +213 -0
  97. data/lib/a2a/server/push_notification_manager.rb +327 -0
  98. data/lib/a2a/server/request_handler.rb +136 -0
  99. data/lib/a2a/server/storage/base.rb +141 -0
  100. data/lib/a2a/server/storage/database.rb +266 -0
  101. data/lib/a2a/server/storage/memory.rb +274 -0
  102. data/lib/a2a/server/storage/redis.rb +320 -0
  103. data/lib/a2a/server/storage.rb +38 -0
  104. data/lib/a2a/server/task_manager.rb +534 -0
  105. data/lib/a2a/transport/grpc.rb +481 -0
  106. data/lib/a2a/transport/http.rb +415 -0
  107. data/lib/a2a/transport/sse.rb +499 -0
  108. data/lib/a2a/types/agent_card.rb +540 -0
  109. data/lib/a2a/types/artifact.rb +99 -0
  110. data/lib/a2a/types/base_model.rb +223 -0
  111. data/lib/a2a/types/events.rb +117 -0
  112. data/lib/a2a/types/message.rb +106 -0
  113. data/lib/a2a/types/part.rb +288 -0
  114. data/lib/a2a/types/push_notification.rb +139 -0
  115. data/lib/a2a/types/security.rb +167 -0
  116. data/lib/a2a/types/task.rb +154 -0
  117. data/lib/a2a/types.rb +88 -0
  118. data/lib/a2a/utils/helpers.rb +245 -0
  119. data/lib/a2a/utils/message_buffer.rb +278 -0
  120. data/lib/a2a/utils/performance.rb +247 -0
  121. data/lib/a2a/utils/rails_detection.rb +97 -0
  122. data/lib/a2a/utils/structured_logger.rb +306 -0
  123. data/lib/a2a/utils/time_helpers.rb +167 -0
  124. data/lib/a2a/utils/validation.rb +8 -0
  125. data/lib/a2a/version.rb +6 -0
  126. data/lib/a2a-rails.rb +58 -0
  127. data/lib/a2a.rb +198 -0
  128. metadata +437 -0
@@ -0,0 +1,490 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+
6
+ module A2A
7
+ module Monitoring
8
+ ##
9
+ # Alerting system for A2A monitoring
10
+ #
11
+ # Provides configurable alerting based on metrics thresholds,
12
+ # error rates, and system health indicators.
13
+ #
14
+ class Alerting
15
+ # Alert severities
16
+ SEVERITY_INFO = :info
17
+ SEVERITY_WARNING = :warning
18
+ SEVERITY_ERROR = :error
19
+ SEVERITY_CRITICAL = :critical
20
+
21
+ # Alert states
22
+ STATE_FIRING = :firing
23
+ STATE_RESOLVED = :resolved
24
+
25
+ attr_reader :rules, :channels, :active_alerts
26
+
27
+ ##
28
+ # Initialize alerting system
29
+ #
30
+ # @param config [Hash] Alerting configuration
31
+ def initialize(config = {})
32
+ @rules = []
33
+ @channels = []
34
+ @active_alerts = {}
35
+ @config = default_config.merge(config)
36
+ @mutex = Mutex.new
37
+ end
38
+
39
+ ##
40
+ # Add an alert rule
41
+ #
42
+ # @param name [String] Rule name
43
+ # @param metric [String] Metric to monitor
44
+ # @param condition [Hash] Alert condition
45
+ # @param severity [Symbol] Alert severity
46
+ # @param description [String] Alert description
47
+ # @param tags [Hash] Additional tags
48
+ def add_rule(name:, metric:, condition:, severity: SEVERITY_WARNING, description: nil, **tags)
49
+ rule = {
50
+ name: name,
51
+ metric: metric,
52
+ condition: condition,
53
+ severity: severity,
54
+ description: description || "Alert for #{metric}",
55
+ tags: tags,
56
+ created_at: Time.now,
57
+ last_evaluated: nil,
58
+ evaluation_count: 0
59
+ }
60
+
61
+ @mutex.synchronize { @rules << rule }
62
+ end
63
+
64
+ ##
65
+ # Add an alert channel
66
+ #
67
+ # @param channel [Object] Alert channel (webhook, email, etc.)
68
+ def add_channel(channel)
69
+ @mutex.synchronize { @channels << channel }
70
+ end
71
+
72
+ ##
73
+ # Evaluate all alert rules against current metrics
74
+ #
75
+ # @param metrics [Hash] Current metrics snapshot
76
+ def evaluate_rules(metrics)
77
+ @mutex.synchronize do
78
+ @rules.each do |rule|
79
+ evaluate_rule(rule, metrics)
80
+ end
81
+ end
82
+ end
83
+
84
+ ##
85
+ # Get active alerts
86
+ #
87
+ # @return [Array<Hash>] Active alerts
88
+ def get_active_alerts
89
+ @mutex.synchronize { @active_alerts.values.dup }
90
+ end
91
+
92
+ ##
93
+ # Get alert statistics
94
+ #
95
+ # @return [Hash] Alert statistics
96
+ def statistics
97
+ @mutex.synchronize do
98
+ {
99
+ total_rules: @rules.size,
100
+ active_alerts: @active_alerts.size,
101
+ total_channels: @channels.size,
102
+ alerts_by_severity: count_alerts_by_severity,
103
+ evaluation_stats: get_evaluation_stats
104
+ }
105
+ end
106
+ end
107
+
108
+ ##
109
+ # Clear resolved alerts older than specified time
110
+ #
111
+ # @param max_age [Integer] Maximum age in seconds (default: 1 hour)
112
+ def cleanup_resolved_alerts(max_age: 3600)
113
+ cutoff_time = Time.now - max_age
114
+
115
+ @mutex.synchronize do
116
+ @active_alerts.reject! do |_, alert|
117
+ alert[:state] == STATE_RESOLVED && alert[:resolved_at] < cutoff_time
118
+ end
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ ##
125
+ # Default configuration
126
+ #
127
+ # @return [Hash] Default configuration
128
+ def default_config
129
+ {
130
+ evaluation_interval: 60,
131
+ alert_timeout: 300,
132
+ max_alerts: 1000
133
+ }
134
+ end
135
+
136
+ ##
137
+ # Evaluate a single alert rule
138
+ #
139
+ # @param rule [Hash] Alert rule
140
+ # @param metrics [Hash] Current metrics
141
+ def evaluate_rule(rule, metrics)
142
+ rule[:last_evaluated] = Time.now
143
+ rule[:evaluation_count] += 1
144
+
145
+ # Find matching metrics
146
+ matching_metrics = find_matching_metrics(rule[:metric], metrics)
147
+
148
+ matching_metrics.each do |metric_key, metric_data|
149
+ evaluate_metric_against_rule(rule, metric_key, metric_data)
150
+ end
151
+ end
152
+
153
+ ##
154
+ # Find metrics matching the rule pattern
155
+ #
156
+ # @param pattern [String] Metric pattern
157
+ # @param metrics [Hash] All metrics
158
+ # @return [Hash] Matching metrics
159
+ def find_matching_metrics(pattern, metrics)
160
+ if pattern.include?("*")
161
+ # Pattern matching
162
+ regex = Regexp.new(pattern.gsub("*", ".*"))
163
+ metrics.select { |key, _| key.match?(regex) }
164
+ else
165
+ # Exact match
166
+ metric_data = metrics[pattern]
167
+ metric_data ? { pattern => metric_data } : {}
168
+ end
169
+ end
170
+
171
+ ##
172
+ # Evaluate a specific metric against a rule
173
+ #
174
+ # @param rule [Hash] Alert rule
175
+ # @param metric_key [String] Metric key
176
+ # @param metric_data [Hash] Metric data
177
+ def evaluate_metric_against_rule(rule, metric_key, metric_data)
178
+ alert_key = "#{rule[:name]}_#{metric_key}"
179
+ condition = rule[:condition]
180
+
181
+ # Extract value based on metric type
182
+ value = extract_metric_value(metric_data, condition[:field])
183
+ return unless value
184
+
185
+ # Evaluate condition
186
+ should_fire = evaluate_condition(condition, value)
187
+
188
+ if should_fire
189
+ fire_alert(alert_key, rule, metric_key, value)
190
+ else
191
+ resolve_alert(alert_key)
192
+ end
193
+ end
194
+
195
+ ##
196
+ # Extract value from metric data
197
+ #
198
+ # @param metric_data [Hash] Metric data
199
+ # @param field [String, nil] Specific field to extract
200
+ # @return [Numeric, nil] Extracted value
201
+ def extract_metric_value(metric_data, field = nil)
202
+ case metric_data[:type]
203
+ when :counter, :gauge
204
+ metric_data[:value]
205
+ when :histogram
206
+ case field
207
+ when "avg", "average" then metric_data[:avg]
208
+ when "p95" then metric_data[:p95]
209
+ when "p99" then metric_data[:p99]
210
+ when "max" then metric_data[:max]
211
+ when "min" then metric_data[:min]
212
+ when "count" then metric_data[:count]
213
+ else metric_data[:avg] # Default to average
214
+ end
215
+ else
216
+ metric_data[:value]
217
+ end
218
+ end
219
+
220
+ ##
221
+ # Evaluate alert condition
222
+ #
223
+ # @param condition [Hash] Alert condition
224
+ # @param value [Numeric] Current value
225
+ # @return [Boolean] Whether condition is met
226
+ def evaluate_condition(condition, value)
227
+ operator = condition[:operator] || condition[:op]
228
+ threshold = condition[:threshold]
229
+
230
+ case operator.to_s
231
+ when "gt", ">" then value > threshold
232
+ when "gte", ">=" then value >= threshold
233
+ when "lt", "<" then value < threshold
234
+ when "lte", "<=" then value <= threshold
235
+ when "eq", "==" then value == threshold
236
+ when "ne", "!=" then value != threshold
237
+ else false
238
+ end
239
+ end
240
+
241
+ ##
242
+ # Fire an alert
243
+ #
244
+ # @param alert_key [String] Alert key
245
+ # @param rule [Hash] Alert rule
246
+ # @param metric_key [String] Metric key
247
+ # @param value [Numeric] Current value
248
+ def fire_alert(alert_key, rule, metric_key, value)
249
+ existing_alert = @active_alerts[alert_key]
250
+
251
+ # Don't fire if already active and within timeout
252
+ if existing_alert && existing_alert[:state] == STATE_FIRING
253
+ time_since_fired = Time.now - existing_alert[:fired_at]
254
+ return if time_since_fired < @config[:alert_timeout]
255
+ end
256
+
257
+ alert = {
258
+ key: alert_key,
259
+ rule_name: rule[:name],
260
+ metric: metric_key,
261
+ value: value,
262
+ condition: rule[:condition],
263
+ severity: rule[:severity],
264
+ description: rule[:description],
265
+ tags: rule[:tags],
266
+ state: STATE_FIRING,
267
+ fired_at: Time.now,
268
+ resolved_at: nil
269
+ }
270
+
271
+ @active_alerts[alert_key] = alert
272
+ send_alert_notification(alert)
273
+ end
274
+
275
+ ##
276
+ # Resolve an alert
277
+ #
278
+ # @param alert_key [String] Alert key
279
+ def resolve_alert(alert_key)
280
+ alert = @active_alerts[alert_key]
281
+ return unless alert && alert[:state] == STATE_FIRING
282
+
283
+ alert[:state] = STATE_RESOLVED
284
+ alert[:resolved_at] = Time.now
285
+
286
+ send_alert_notification(alert)
287
+ end
288
+
289
+ ##
290
+ # Send alert notification to all channels
291
+ #
292
+ # @param alert [Hash] Alert data
293
+ def send_alert_notification(alert)
294
+ @channels.each do |channel|
295
+ channel.send_alert(alert)
296
+ rescue StandardError => e
297
+ warn "Failed to send alert via #{channel.class}: #{e.message}"
298
+ end
299
+ end
300
+
301
+ ##
302
+ # Count alerts by severity
303
+ #
304
+ # @return [Hash] Alert counts by severity
305
+ def count_alerts_by_severity
306
+ counts = Hash.new(0)
307
+
308
+ @active_alerts.each_value do |alert|
309
+ next unless alert[:state] == STATE_FIRING
310
+
311
+ counts[alert[:severity]] += 1
312
+ end
313
+
314
+ counts
315
+ end
316
+
317
+ ##
318
+ # Get evaluation statistics
319
+ #
320
+ # @return [Hash] Evaluation statistics
321
+ def get_evaluation_stats
322
+ return {} if @rules.empty?
323
+
324
+ total_evaluations = @rules.sum { |rule| rule[:evaluation_count] }
325
+ avg_evaluations = total_evaluations.to_f / @rules.size
326
+
327
+ {
328
+ total_evaluations: total_evaluations,
329
+ average_evaluations_per_rule: avg_evaluations,
330
+ last_evaluation: @rules.pluck(:last_evaluated).compact.max
331
+ }
332
+ end
333
+ end
334
+
335
+ ##
336
+ # Webhook alert channel
337
+ #
338
+ class WebhookAlertChannel
339
+ attr_reader :url, :headers
340
+
341
+ ##
342
+ # Initialize webhook channel
343
+ #
344
+ # @param url [String] Webhook URL
345
+ # @param headers [Hash] HTTP headers
346
+ # @param timeout [Integer] Request timeout
347
+ def initialize(url, headers: {}, timeout: 10)
348
+ @url = url
349
+ @headers = headers
350
+ @timeout = timeout
351
+ end
352
+
353
+ ##
354
+ # Send alert via webhook
355
+ #
356
+ # @param alert [Hash] Alert data
357
+ def send_alert(alert)
358
+ payload = format_alert_payload(alert)
359
+
360
+ uri = URI(@url)
361
+ http = Net::HTTP.new(uri.host, uri.port)
362
+ http.use_ssl = uri.scheme == "https"
363
+ http.read_timeout = @timeout
364
+
365
+ request = Net::HTTP::Post.new(uri.path)
366
+ request["Content-Type"] = "application/json"
367
+ @headers.each { |key, value| request[key] = value }
368
+ request.body = payload.to_json
369
+
370
+ response = http.request(request)
371
+
372
+ return if response.is_a?(Net::HTTPSuccess)
373
+
374
+ raise "Webhook request failed: #{response.code} #{response.message}"
375
+ end
376
+
377
+ private
378
+
379
+ ##
380
+ # Format alert for webhook payload
381
+ #
382
+ # @param alert [Hash] Alert data
383
+ # @return [Hash] Formatted payload
384
+ def format_alert_payload(alert)
385
+ {
386
+ alert_name: alert[:rule_name],
387
+ metric: alert[:metric],
388
+ value: alert[:value],
389
+ severity: alert[:severity],
390
+ state: alert[:state],
391
+ description: alert[:description],
392
+ fired_at: alert[:fired_at]&.iso8601,
393
+ resolved_at: alert[:resolved_at]&.iso8601,
394
+ tags: alert[:tags]
395
+ }
396
+ end
397
+ end
398
+
399
+ ##
400
+ # Slack alert channel
401
+ #
402
+ class SlackAlertChannel
403
+ ##
404
+ # Initialize Slack channel
405
+ #
406
+ # @param webhook_url [String] Slack webhook URL
407
+ # @param channel [String, nil] Slack channel name
408
+ # @param username [String] Bot username
409
+ def initialize(webhook_url, channel: nil, username: "A2A Alerts")
410
+ @webhook_url = webhook_url
411
+ @channel = channel
412
+ @username = username
413
+ end
414
+
415
+ ##
416
+ # Send alert to Slack
417
+ #
418
+ # @param alert [Hash] Alert data
419
+ def send_alert(alert)
420
+ payload = {
421
+ username: @username,
422
+ channel: @channel,
423
+ attachments: [format_slack_attachment(alert)]
424
+ }.compact
425
+
426
+ uri = URI(@webhook_url)
427
+ http = Net::HTTP.new(uri.host, uri.port)
428
+ http.use_ssl = true
429
+
430
+ request = Net::HTTP::Post.new(uri.path)
431
+ request["Content-Type"] = "application/json"
432
+ request.body = payload.to_json
433
+
434
+ response = http.request(request)
435
+
436
+ return if response.is_a?(Net::HTTPSuccess)
437
+
438
+ raise "Slack webhook request failed: #{response.code}"
439
+ end
440
+
441
+ private
442
+
443
+ ##
444
+ # Format alert as Slack attachment
445
+ #
446
+ # @param alert [Hash] Alert data
447
+ # @return [Hash] Slack attachment
448
+ def format_slack_attachment(alert)
449
+ color = case alert[:severity]
450
+ when :critical then "danger"
451
+ when :error then "danger"
452
+ when :warning then "warning"
453
+ else "good"
454
+ end
455
+
456
+ color = "good" if alert[:state] == :resolved
457
+
458
+ {
459
+ color: color,
460
+ title: "#{alert[:state].to_s.capitalize}: #{alert[:rule_name]}",
461
+ text: alert[:description],
462
+ fields: [
463
+ {
464
+ title: "Metric",
465
+ value: alert[:metric],
466
+ short: true
467
+ },
468
+ {
469
+ title: "Value",
470
+ value: alert[:value].to_s,
471
+ short: true
472
+ },
473
+ {
474
+ title: "Severity",
475
+ value: alert[:severity].to_s.capitalize,
476
+ short: true
477
+ },
478
+ {
479
+ title: "Time",
480
+ value: (alert[:fired_at] || alert[:resolved_at]).strftime("%Y-%m-%d %H:%M:%S UTC"),
481
+ short: true
482
+ }
483
+ ],
484
+ footer: "A2A Monitoring",
485
+ ts: (alert[:fired_at] || alert[:resolved_at]).to_i
486
+ }
487
+ end
488
+ end
489
+ end
490
+ end