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,228 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# <%= model_class_name("push_notification_config") %>
|
5
|
+
#
|
6
|
+
# ActiveRecord model for A2A push notification configurations.
|
7
|
+
# This model stores webhook URLs and authentication details for task notifications.
|
8
|
+
#
|
9
|
+
class <%= model_class_name("push_notification_config") %> < ApplicationRecord
|
10
|
+
self.table_name = "<%= push_notification_configs_table_name %>"
|
11
|
+
self.primary_key = "id"
|
12
|
+
|
13
|
+
# Associations
|
14
|
+
belongs_to :<%= model_file_name("task") %>,
|
15
|
+
foreign_key: :task_id,
|
16
|
+
class_name: "<%= model_class_name("task") %>"
|
17
|
+
|
18
|
+
# Validations
|
19
|
+
validates :id, presence: true, uniqueness: true
|
20
|
+
validates :task_id, presence: true
|
21
|
+
validates :url, presence: true, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]) }
|
22
|
+
validates :retry_count, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
23
|
+
|
24
|
+
# Scopes
|
25
|
+
scope :active, -> { where(active: true, deleted_at: nil) }
|
26
|
+
scope :inactive, -> { where(active: false) }
|
27
|
+
scope :by_task, ->(task_id) { where(task_id: task_id) }
|
28
|
+
scope :failed_recently, -> { where("last_failure_at > ?", 1.hour.ago) }
|
29
|
+
scope :needs_retry, -> { where("retry_count < ? AND (last_failure_at IS NULL OR last_failure_at < ?)", max_retries, retry_delay.ago) }
|
30
|
+
|
31
|
+
# Callbacks
|
32
|
+
before_create :ensure_id
|
33
|
+
after_create :log_creation
|
34
|
+
after_update :log_status_change, if: :saved_change_to_active?
|
35
|
+
|
36
|
+
# Configuration
|
37
|
+
MAX_RETRIES = 5
|
38
|
+
RETRY_DELAY = 5.minutes
|
39
|
+
|
40
|
+
def self.max_retries
|
41
|
+
MAX_RETRIES
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.retry_delay
|
45
|
+
RETRY_DELAY
|
46
|
+
end
|
47
|
+
|
48
|
+
# Soft delete
|
49
|
+
def soft_delete!
|
50
|
+
update!(deleted_at: Time.now, active: false)
|
51
|
+
end
|
52
|
+
|
53
|
+
def deleted?
|
54
|
+
deleted_at.present?
|
55
|
+
end
|
56
|
+
|
57
|
+
# Status management
|
58
|
+
def mark_success!
|
59
|
+
update!(
|
60
|
+
last_success_at: Time.now,
|
61
|
+
last_failure_at: nil,
|
62
|
+
last_error: nil,
|
63
|
+
retry_count: 0
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
def mark_failure!(error_message)
|
68
|
+
update!(
|
69
|
+
last_failure_at: Time.now,
|
70
|
+
last_error: error_message,
|
71
|
+
retry_count: retry_count + 1,
|
72
|
+
active: retry_count < self.class.max_retries
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
def can_retry?
|
77
|
+
active && retry_count < self.class.max_retries &&
|
78
|
+
(last_failure_at.nil? || last_failure_at < self.class.retry_delay.ago)
|
79
|
+
end
|
80
|
+
|
81
|
+
def should_disable?
|
82
|
+
retry_count >= self.class.max_retries
|
83
|
+
end
|
84
|
+
|
85
|
+
# Convert to A2A types
|
86
|
+
def to_a2a_push_notification_config
|
87
|
+
A2A::Types::PushNotificationConfig.new(
|
88
|
+
id: id,
|
89
|
+
url: url,
|
90
|
+
token: token,
|
91
|
+
authentication: authentication || {}
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_a2a_task_push_notification_config
|
96
|
+
A2A::Types::TaskPushNotificationConfig.new(
|
97
|
+
task_id: task_id,
|
98
|
+
push_notification_config: to_a2a_push_notification_config
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Create from A2A types
|
103
|
+
def self.from_a2a_config(task_id, config)
|
104
|
+
if config.is_a?(A2A::Types::TaskPushNotificationConfig)
|
105
|
+
pn_config = config.push_notification_config
|
106
|
+
task_id = config.task_id
|
107
|
+
elsif config.is_a?(A2A::Types::PushNotificationConfig)
|
108
|
+
pn_config = config
|
109
|
+
else
|
110
|
+
raise ArgumentError, "Invalid config type"
|
111
|
+
end
|
112
|
+
|
113
|
+
new(
|
114
|
+
id: pn_config.id || SecureRandom.uuid,
|
115
|
+
task_id: task_id,
|
116
|
+
url: pn_config.url,
|
117
|
+
token: pn_config.token,
|
118
|
+
authentication: pn_config.authentication || {}
|
119
|
+
)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Webhook delivery
|
123
|
+
def deliver_notification(event_data)
|
124
|
+
return false unless active? && !deleted?
|
125
|
+
|
126
|
+
begin
|
127
|
+
response = send_webhook_request(event_data)
|
128
|
+
|
129
|
+
if response.success?
|
130
|
+
mark_success!
|
131
|
+
true
|
132
|
+
else
|
133
|
+
mark_failure!("HTTP #{response.code}: #{response.body}")
|
134
|
+
false
|
135
|
+
end
|
136
|
+
rescue => e
|
137
|
+
mark_failure!(e.message)
|
138
|
+
false
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Authentication helpers
|
143
|
+
def has_authentication?
|
144
|
+
authentication.present? && authentication.any?
|
145
|
+
end
|
146
|
+
|
147
|
+
def authentication_type
|
148
|
+
return nil unless has_authentication?
|
149
|
+
|
150
|
+
if authentication["type"].present?
|
151
|
+
authentication["type"]
|
152
|
+
elsif token.present?
|
153
|
+
"bearer"
|
154
|
+
else
|
155
|
+
"custom"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Statistics
|
160
|
+
def success_rate
|
161
|
+
total_attempts = retry_count + (last_success_at.present? ? 1 : 0)
|
162
|
+
return 0.0 if total_attempts == 0
|
163
|
+
|
164
|
+
successful_attempts = last_success_at.present? ? 1 : 0
|
165
|
+
(successful_attempts.to_f / total_attempts * 100).round(2)
|
166
|
+
end
|
167
|
+
|
168
|
+
def last_activity
|
169
|
+
[last_success_at, last_failure_at].compact.max
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
def ensure_id
|
175
|
+
self.id ||= SecureRandom.uuid
|
176
|
+
end
|
177
|
+
|
178
|
+
def log_creation
|
179
|
+
Rails.logger.info "Created push notification config #{id} for task #{task_id}"
|
180
|
+
end
|
181
|
+
|
182
|
+
def log_status_change
|
183
|
+
status = active? ? "activated" : "deactivated"
|
184
|
+
Rails.logger.info "Push notification config #{id} #{status}"
|
185
|
+
end
|
186
|
+
|
187
|
+
def send_webhook_request(event_data)
|
188
|
+
require 'faraday'
|
189
|
+
|
190
|
+
conn = Faraday.new do |f|
|
191
|
+
f.request :json
|
192
|
+
f.response :json
|
193
|
+
f.adapter Faraday.default_adapter
|
194
|
+
end
|
195
|
+
|
196
|
+
headers = build_request_headers
|
197
|
+
|
198
|
+
conn.post(url) do |req|
|
199
|
+
req.headers.merge!(headers)
|
200
|
+
req.body = event_data
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def build_request_headers
|
205
|
+
headers = {
|
206
|
+
'Content-Type' => 'application/json',
|
207
|
+
'User-Agent' => "A2A-Ruby/#{A2A::VERSION}",
|
208
|
+
'X-A2A-Task-ID' => task_id,
|
209
|
+
'X-A2A-Config-ID' => id
|
210
|
+
}
|
211
|
+
|
212
|
+
# Add authentication headers
|
213
|
+
case authentication_type
|
214
|
+
when "bearer"
|
215
|
+
headers['Authorization'] = "Bearer #{token}"
|
216
|
+
when "api_key"
|
217
|
+
headers['X-API-Key'] = token
|
218
|
+
when "custom"
|
219
|
+
# Add custom authentication headers from authentication hash
|
220
|
+
authentication.each do |key, value|
|
221
|
+
next if %w[type].include?(key)
|
222
|
+
headers[key] = value
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
headers
|
227
|
+
end
|
228
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# <%= model_class_name("task") %>
|
5
|
+
#
|
6
|
+
# ActiveRecord model for A2A tasks with JSON serialization and validation.
|
7
|
+
# This model provides persistence for A2A task data including status, artifacts,
|
8
|
+
# and message history.
|
9
|
+
#
|
10
|
+
class <%= model_class_name("task") %> < ApplicationRecord
|
11
|
+
self.table_name = "<%= tasks_table_name %>"
|
12
|
+
self.primary_key = "id"
|
13
|
+
|
14
|
+
# Associations
|
15
|
+
has_many :<%= model_file_name("push_notification_config").pluralize %>,
|
16
|
+
foreign_key: :task_id,
|
17
|
+
dependent: :destroy,
|
18
|
+
class_name: "<%= model_class_name("push_notification_config") %>"
|
19
|
+
|
20
|
+
# Validations
|
21
|
+
validates :id, presence: true, uniqueness: true
|
22
|
+
validates :context_id, presence: true
|
23
|
+
validates :kind, presence: true, inclusion: { in: %w[task] }
|
24
|
+
validates :status_state, presence: true, inclusion: {
|
25
|
+
in: %w[submitted working input-required completed canceled failed rejected auth-required unknown]
|
26
|
+
}
|
27
|
+
|
28
|
+
# Scopes
|
29
|
+
scope :active, -> { where(deleted_at: nil) }
|
30
|
+
scope :by_status, ->(status) { where(status_state: status) }
|
31
|
+
scope :by_context, ->(context_id) { where(context_id: context_id) }
|
32
|
+
scope :by_type, ->(type) { where(type: type) }
|
33
|
+
scope :recent, -> { order(created_at: :desc) }
|
34
|
+
scope :processing, -> { where(status_state: %w[submitted working]) }
|
35
|
+
scope :completed, -> { where(status_state: %w[completed canceled failed rejected]) }
|
36
|
+
|
37
|
+
# Callbacks
|
38
|
+
before_create :ensure_id
|
39
|
+
before_save :update_status_timestamp
|
40
|
+
after_update :notify_status_change, if: :saved_change_to_status_state?
|
41
|
+
|
42
|
+
# Soft delete
|
43
|
+
def soft_delete!
|
44
|
+
update!(deleted_at: Time.now)
|
45
|
+
end
|
46
|
+
|
47
|
+
def deleted?
|
48
|
+
deleted_at.present?
|
49
|
+
end
|
50
|
+
|
51
|
+
# Status management
|
52
|
+
def status
|
53
|
+
A2A::Types::TaskStatus.new(
|
54
|
+
state: status_state,
|
55
|
+
message: status_message,
|
56
|
+
progress: status_progress,
|
57
|
+
result: status_result,
|
58
|
+
error: status_error,
|
59
|
+
updated_at: status_updated_at&.iso8601
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
def status=(new_status)
|
64
|
+
if new_status.is_a?(A2A::Types::TaskStatus)
|
65
|
+
self.status_state = new_status.state
|
66
|
+
self.status_message = new_status.message
|
67
|
+
self.status_progress = new_status.progress
|
68
|
+
self.status_result = new_status.result
|
69
|
+
self.status_error = new_status.error
|
70
|
+
self.status_updated_at = Time.now
|
71
|
+
elsif new_status.is_a?(Hash)
|
72
|
+
self.status = A2A::Types::TaskStatus.from_h(new_status)
|
73
|
+
else
|
74
|
+
raise ArgumentError, "Status must be TaskStatus or Hash"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Artifact management
|
79
|
+
def artifacts_objects
|
80
|
+
return [] unless artifacts.present?
|
81
|
+
|
82
|
+
artifacts.map { |artifact_data| A2A::Types::Artifact.from_h(artifact_data) }
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_artifact(artifact)
|
86
|
+
artifact_data = artifact.is_a?(A2A::Types::Artifact) ? artifact.to_h : artifact
|
87
|
+
|
88
|
+
self.artifacts = (artifacts || []) + [artifact_data]
|
89
|
+
save!
|
90
|
+
end
|
91
|
+
|
92
|
+
def update_artifact(artifact_id, new_artifact, append: false)
|
93
|
+
return unless artifacts.present?
|
94
|
+
|
95
|
+
artifact_index = artifacts.find_index { |a| a["artifact_id"] == artifact_id }
|
96
|
+
return unless artifact_index
|
97
|
+
|
98
|
+
new_artifact_data = new_artifact.is_a?(A2A::Types::Artifact) ? new_artifact.to_h : new_artifact
|
99
|
+
|
100
|
+
if append && artifacts[artifact_index]["parts"].present?
|
101
|
+
# Append parts to existing artifact
|
102
|
+
existing_parts = artifacts[artifact_index]["parts"] || []
|
103
|
+
new_parts = new_artifact_data["parts"] || []
|
104
|
+
artifacts[artifact_index]["parts"] = existing_parts + new_parts
|
105
|
+
else
|
106
|
+
# Replace entire artifact
|
107
|
+
artifacts[artifact_index] = new_artifact_data
|
108
|
+
end
|
109
|
+
|
110
|
+
save!
|
111
|
+
end
|
112
|
+
|
113
|
+
# Message history management
|
114
|
+
def history_objects
|
115
|
+
return [] unless history.present?
|
116
|
+
|
117
|
+
history.map { |message_data| A2A::Types::Message.from_h(message_data) }
|
118
|
+
end
|
119
|
+
|
120
|
+
def add_message(message)
|
121
|
+
message_data = message.is_a?(A2A::Types::Message) ? message.to_h : message
|
122
|
+
|
123
|
+
self.history = (history || []) + [message_data]
|
124
|
+
|
125
|
+
# Limit history length if configured
|
126
|
+
max_history = A2A.config.max_history_length || 100
|
127
|
+
if history.length > max_history
|
128
|
+
self.history = history.last(max_history)
|
129
|
+
end
|
130
|
+
|
131
|
+
save!
|
132
|
+
end
|
133
|
+
|
134
|
+
# Convert to A2A::Types::Task
|
135
|
+
def to_a2a_task
|
136
|
+
A2A::Types::Task.new(
|
137
|
+
id: id,
|
138
|
+
context_id: context_id,
|
139
|
+
kind: kind,
|
140
|
+
status: status,
|
141
|
+
artifacts: artifacts_objects,
|
142
|
+
history: history_objects,
|
143
|
+
metadata: metadata || {}
|
144
|
+
)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Create from A2A::Types::Task
|
148
|
+
def self.from_a2a_task(task)
|
149
|
+
new(
|
150
|
+
id: task.id,
|
151
|
+
context_id: task.context_id,
|
152
|
+
kind: task.kind,
|
153
|
+
status_state: task.status.state,
|
154
|
+
status_message: task.status.message,
|
155
|
+
status_progress: task.status.progress,
|
156
|
+
status_result: task.status.result,
|
157
|
+
status_error: task.status.error,
|
158
|
+
status_updated_at: task.status.updated_at ? Time.parse(task.status.updated_at) : Time.now,
|
159
|
+
artifacts: task.artifacts&.map(&:to_h),
|
160
|
+
history: task.history&.map(&:to_h),
|
161
|
+
metadata: task.metadata
|
162
|
+
)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Search and filtering
|
166
|
+
def self.search(query)
|
167
|
+
return all if query.nil? || (respond_to?(:empty?) && empty?) || (is_a?(String) && strip.empty?)
|
168
|
+
|
169
|
+
where(
|
170
|
+
"status_message ILIKE ? OR metadata::text ILIKE ?",
|
171
|
+
"%#{query}%", "%#{query}%"
|
172
|
+
)
|
173
|
+
end
|
174
|
+
|
175
|
+
def self.by_metadata(key, value)
|
176
|
+
<% if postgresql? %>
|
177
|
+
where("metadata->? = ?", key, value.to_json)
|
178
|
+
<% else %>
|
179
|
+
where("JSON_EXTRACT(metadata, ?) = ?", "$.#{key}", value.to_s)
|
180
|
+
<% end %>
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
|
185
|
+
def ensure_id
|
186
|
+
self.id ||= SecureRandom.uuid
|
187
|
+
self.context_id ||= SecureRandom.uuid
|
188
|
+
end
|
189
|
+
|
190
|
+
def update_status_timestamp
|
191
|
+
if status_state_changed?
|
192
|
+
self.status_updated_at = Time.now
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def notify_status_change
|
197
|
+
# Trigger push notifications and events
|
198
|
+
A2A::Server::TaskManager.instance.notify_task_status_change(self) if A2A.config.push_notifications_enabled
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :a2a do
|
4
|
+
desc "Show A2A configuration"
|
5
|
+
task config: :environment do
|
6
|
+
puts "A2A Configuration:"
|
7
|
+
puts "=================="
|
8
|
+
puts "Rails Integration: #{A2A.config.rails_integration}"
|
9
|
+
puts "Mount Path: #{A2A.config.mount_path}"
|
10
|
+
puts "Authentication Required: #{A2A.config.authentication_required}"
|
11
|
+
puts "CORS Enabled: #{A2A.config.cors_enabled}"
|
12
|
+
puts "Rate Limiting Enabled: #{A2A.config.rate_limiting_enabled}"
|
13
|
+
puts "Logging Enabled: #{A2A.config.logging_enabled}"
|
14
|
+
puts "Version: #{A2A::VERSION}"
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "List all registered A2A agents and capabilities"
|
18
|
+
task agents: :environment do
|
19
|
+
puts "Registered A2A Agents:"
|
20
|
+
puts "====================="
|
21
|
+
|
22
|
+
agent_count = 0
|
23
|
+
capability_count = 0
|
24
|
+
|
25
|
+
ObjectSpace.each_object(Class) do |klass|
|
26
|
+
next unless klass < ActionController::Base && klass.included_modules.include?(A2A::Server::Agent)
|
27
|
+
|
28
|
+
agent_count += 1
|
29
|
+
puts "\n#{klass.name}:"
|
30
|
+
|
31
|
+
capabilities = klass._a2a_capabilities || []
|
32
|
+
capability_count += capabilities.length
|
33
|
+
|
34
|
+
if capabilities.any?
|
35
|
+
capabilities.each do |capability|
|
36
|
+
puts " - #{capability.name}: #{capability.description}"
|
37
|
+
end
|
38
|
+
else
|
39
|
+
puts " (no capabilities defined)"
|
40
|
+
end
|
41
|
+
|
42
|
+
methods = klass._a2a_methods || {}
|
43
|
+
next unless methods.any?
|
44
|
+
|
45
|
+
puts " Methods:"
|
46
|
+
methods.each_key do |method_name|
|
47
|
+
puts " - #{method_name}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
puts "\nSummary:"
|
52
|
+
puts "--------"
|
53
|
+
puts "Total Agents: #{agent_count}"
|
54
|
+
puts "Total Capabilities: #{capability_count}"
|
55
|
+
end
|
56
|
+
|
57
|
+
desc "Generate agent card for all registered agents"
|
58
|
+
task agent_cards: :environment do
|
59
|
+
puts "Agent Cards:"
|
60
|
+
puts "============"
|
61
|
+
|
62
|
+
ObjectSpace.each_object(Class) do |klass|
|
63
|
+
next unless klass < ActionController::Base && klass.included_modules.include?(A2A::Server::Agent)
|
64
|
+
|
65
|
+
puts "\n#{klass.name}:"
|
66
|
+
puts "-" * (klass.name.length + 1)
|
67
|
+
|
68
|
+
begin
|
69
|
+
# Create a mock controller instance to generate the card
|
70
|
+
controller = klass.new
|
71
|
+
controller.request = ActionDispatch::Request.new({})
|
72
|
+
|
73
|
+
card = controller.send(:generate_agent_card)
|
74
|
+
puts JSON.pretty_generate(card.to_h)
|
75
|
+
rescue StandardError => e
|
76
|
+
puts "Error generating card: #{e.message}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
desc "Validate A2A protocol compliance"
|
82
|
+
task validate: :environment do
|
83
|
+
puts "A2A Protocol Validation:"
|
84
|
+
puts "======================="
|
85
|
+
|
86
|
+
errors = []
|
87
|
+
warnings = []
|
88
|
+
|
89
|
+
# Check Rails version compatibility
|
90
|
+
errors << "Rails version #{Rails.version} is not supported. Minimum version is 6.0" if Rails.version < "6.0"
|
91
|
+
|
92
|
+
# Check required dependencies
|
93
|
+
required_gems = %w[faraday json jwt redis concurrent-ruby]
|
94
|
+
required_gems.each do |gem_name|
|
95
|
+
require gem_name
|
96
|
+
rescue LoadError
|
97
|
+
errors << "Required gem '#{gem_name}' is not available"
|
98
|
+
end
|
99
|
+
|
100
|
+
# Validate configuration
|
101
|
+
config = A2A.config
|
102
|
+
errors << "Mount path must start with '/'" if config.mount_path && !config.mount_path.start_with?("/")
|
103
|
+
|
104
|
+
# Check for registered agents
|
105
|
+
agent_classes = []
|
106
|
+
ObjectSpace.each_object(Class) do |klass|
|
107
|
+
agent_classes << klass if klass < ActionController::Base && klass.included_modules.include?(A2A::Server::Agent)
|
108
|
+
end
|
109
|
+
|
110
|
+
warnings << "No A2A agents found. Consider creating some with 'rails generate a2a:agent'" if agent_classes.empty?
|
111
|
+
|
112
|
+
# Validate agent implementations
|
113
|
+
agent_classes.each do |klass|
|
114
|
+
capabilities = klass._a2a_capabilities || []
|
115
|
+
methods = klass._a2a_methods || {}
|
116
|
+
|
117
|
+
warnings << "#{klass.name} has no capabilities defined" if capabilities.empty?
|
118
|
+
|
119
|
+
warnings << "#{klass.name} has no A2A methods defined" if methods.empty?
|
120
|
+
end
|
121
|
+
|
122
|
+
# Report results
|
123
|
+
if errors.any?
|
124
|
+
puts "❌ Validation failed with #{errors.length} error(s):"
|
125
|
+
errors.each { |error| puts " - #{error}" }
|
126
|
+
else
|
127
|
+
puts "✅ Validation passed!"
|
128
|
+
end
|
129
|
+
|
130
|
+
if warnings.any?
|
131
|
+
puts "\n⚠️ #{warnings.length} warning(s):"
|
132
|
+
warnings.each { |warning| puts " - #{warning}" }
|
133
|
+
end
|
134
|
+
|
135
|
+
puts "\nSummary:"
|
136
|
+
puts "--------"
|
137
|
+
puts "Agents found: #{agent_classes.length}"
|
138
|
+
puts "Total capabilities: #{agent_classes.sum { |k| (k._a2a_capabilities || []).length }}"
|
139
|
+
puts "Total methods: #{agent_classes.sum { |k| (k._a2a_methods || {}).length }}"
|
140
|
+
end
|
141
|
+
|
142
|
+
desc "Start A2A development server"
|
143
|
+
task server: :environment do
|
144
|
+
puts "Starting A2A development server..."
|
145
|
+
puts "A2A endpoints will be available at: #{A2A.config.mount_path}"
|
146
|
+
puts "Press Ctrl+C to stop"
|
147
|
+
|
148
|
+
# This would typically start a development server
|
149
|
+
# For now, just show the configuration
|
150
|
+
Rake::Task["a2a:config"].invoke
|
151
|
+
puts "\nUse 'rails server' to start the full Rails application"
|
152
|
+
end
|
153
|
+
|
154
|
+
namespace :db do
|
155
|
+
desc "Create A2A database tables"
|
156
|
+
task migrate: :environment do
|
157
|
+
puts "Creating A2A database tables..."
|
158
|
+
|
159
|
+
# Check if migrations exist
|
160
|
+
migration_path = Rails.root.join("db", "migrate")
|
161
|
+
a2a_migrations = Dir.glob(migration_path.join("*_create_a2a_*.rb"))
|
162
|
+
|
163
|
+
if a2a_migrations.empty?
|
164
|
+
puts "No A2A migrations found. Run 'rails generate a2a:migration' first."
|
165
|
+
else
|
166
|
+
puts "Found #{a2a_migrations.length} A2A migration(s)"
|
167
|
+
Rake::Task["db:migrate"].invoke
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
desc "Seed A2A database with sample data"
|
172
|
+
task seed: :environment do
|
173
|
+
puts "Seeding A2A database..."
|
174
|
+
|
175
|
+
# Create sample tasks for development
|
176
|
+
if Rails.env.development?
|
177
|
+
task_manager = A2A::Server::TaskManager.instance
|
178
|
+
|
179
|
+
3.times do |i|
|
180
|
+
task = task_manager.create_task(
|
181
|
+
type: "sample_task_#{i + 1}",
|
182
|
+
params: { message: "Sample task #{i + 1}" }
|
183
|
+
)
|
184
|
+
puts "Created sample task: #{task.id}"
|
185
|
+
end
|
186
|
+
else
|
187
|
+
puts "Seeding is only available in development environment"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
namespace :test do
|
193
|
+
desc "Run A2A protocol compliance tests"
|
194
|
+
task compliance: :environment do
|
195
|
+
puts "Running A2A protocol compliance tests..."
|
196
|
+
|
197
|
+
# This would run specific compliance tests
|
198
|
+
# For now, just validate the setup
|
199
|
+
Rake::Task["a2a:validate"].invoke
|
200
|
+
end
|
201
|
+
|
202
|
+
desc "Test A2A endpoints"
|
203
|
+
task endpoints: :environment do
|
204
|
+
puts "Testing A2A endpoints..."
|
205
|
+
|
206
|
+
require "net/http"
|
207
|
+
require "uri"
|
208
|
+
|
209
|
+
base_url = "http://localhost:3000#{A2A.config.mount_path}"
|
210
|
+
|
211
|
+
endpoints = [
|
212
|
+
{ path: "/health", method: "GET" },
|
213
|
+
{ path: "/agent-card", method: "GET" },
|
214
|
+
{ path: "/capabilities", method: "GET" }
|
215
|
+
]
|
216
|
+
|
217
|
+
endpoints.each do |endpoint|
|
218
|
+
uri = URI("#{base_url}#{endpoint[:path]}")
|
219
|
+
response = Net::HTTP.get_response(uri)
|
220
|
+
|
221
|
+
status = response.code.to_i < 400 ? "✅" : "❌"
|
222
|
+
puts "#{status} #{endpoint[:method]} #{endpoint[:path]} - #{response.code}"
|
223
|
+
rescue StandardError => e
|
224
|
+
puts "❌ #{endpoint[:method]} #{endpoint[:path]} - Error: #{e.message}"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|