agent99 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.envrc +3 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +21 -0
- data/README.md +99 -0
- data/Rakefile +8 -0
- data/docs/todo.md +66 -0
- data/examples/README.md +79 -0
- data/examples/hello_world.rb +97 -0
- data/examples/hello_world_client.rb +70 -0
- data/examples/hello_world_request.rb +12 -0
- data/examples/registry.rb +81 -0
- data/examples/start_agents.sh +20 -0
- data/lib/agent99/.irbrc +5 -0
- data/lib/agent99/agent_discovery.rb +35 -0
- data/lib/agent99/agent_lifecycle.rb +87 -0
- data/lib/agent99/amqp_message_client.rb +117 -0
- data/lib/agent99/base.rb +61 -0
- data/lib/agent99/control_actions.rb +56 -0
- data/lib/agent99/header_management.rb +24 -0
- data/lib/agent99/header_schema.rb +15 -0
- data/lib/agent99/message_processing.rb +155 -0
- data/lib/agent99/nats_message_client.rb +101 -0
- data/lib/agent99/registry_client.rb +73 -0
- data/lib/agent99/timestamp.rb +36 -0
- data/lib/agent99/version.rb +7 -0
- data/lib/agent99.rb +19 -0
- data/sig/ai_agent.rbs +4 -0
- metadata +205 -0
@@ -0,0 +1,87 @@
|
|
1
|
+
# lib/agent99/agent_lifecycle.rb
|
2
|
+
|
3
|
+
module Agent99::AgentLifecycle
|
4
|
+
|
5
|
+
# Initializes a new AI agent with the given configuration.
|
6
|
+
#
|
7
|
+
# @param registry_client [Agent99::RegistryClient] The client for agent registration
|
8
|
+
# @param message_client [Agent99::AmqpMessageClient] The client for message handling
|
9
|
+
# @param logger [Logger] The logger instance for the agent
|
10
|
+
#
|
11
|
+
def initialize(registry_client: Agent99::RegistryClient.new,
|
12
|
+
message_client: Agent99::AmqpMessageClient.new,
|
13
|
+
logger: Logger.new($stdout))
|
14
|
+
@payload = nil
|
15
|
+
@name = self.class.name
|
16
|
+
@capabilities = capabilities
|
17
|
+
@id = nil
|
18
|
+
@registry_client = registry_client
|
19
|
+
@message_client = message_client
|
20
|
+
@logger = logger
|
21
|
+
|
22
|
+
@registry_client.logger = logger
|
23
|
+
register
|
24
|
+
|
25
|
+
@queue = message_client.setup(agent_id: id, logger:)
|
26
|
+
|
27
|
+
init if respond_to?(:init)
|
28
|
+
|
29
|
+
setup_signal_handlers
|
30
|
+
end
|
31
|
+
|
32
|
+
# Registers the agent with the registry service.
|
33
|
+
#
|
34
|
+
# @raise [StandardError] If registration fails
|
35
|
+
#
|
36
|
+
def register
|
37
|
+
@id = registry_client.register(name:, capabilities:)
|
38
|
+
logger.info "Registered Agent #{name} with ID: #{id}"
|
39
|
+
rescue StandardError => e
|
40
|
+
handle_error("Error during registration", e)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Withdraws the agent from the registry service.
|
44
|
+
#
|
45
|
+
def withdraw
|
46
|
+
registry_client.withdraw(@id) if @id
|
47
|
+
@id = nil
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
################################################
|
52
|
+
private
|
53
|
+
|
54
|
+
# Checks if the agent is currently paused.
|
55
|
+
#
|
56
|
+
# @return [Boolean] True if the agent is paused, false otherwise
|
57
|
+
#
|
58
|
+
def paused?
|
59
|
+
@paused
|
60
|
+
end
|
61
|
+
|
62
|
+
# Sets up signal handlers for graceful shutdown.
|
63
|
+
#
|
64
|
+
def setup_signal_handlers
|
65
|
+
at_exit { fini }
|
66
|
+
|
67
|
+
%w[INT TERM QUIT].each do |signal|
|
68
|
+
Signal.trap(signal) do
|
69
|
+
STDERR.puts "\nReceived #{signal} signal. Initiating graceful shutdown..."
|
70
|
+
exit
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
# Performs cleanup operations when the agent is shutting down.
|
77
|
+
#
|
78
|
+
def fini
|
79
|
+
if id
|
80
|
+
queue_name = id
|
81
|
+
withdraw
|
82
|
+
@message_client&.delete_queue(queue_name)
|
83
|
+
else
|
84
|
+
logger.warn('fini called with a nil id')
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# lib/agent99/amqp_message_client.rb
|
2
|
+
|
3
|
+
require 'bunny'
|
4
|
+
require 'json'
|
5
|
+
require 'json_schema'
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
class Agent99::AmqpMessageClient
|
9
|
+
QUEUE_TTL = 60_000 # 60 seconds TTL
|
10
|
+
@instance = nil
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def instance
|
14
|
+
@instance ||= new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_accessor :logger, :channel, :exchange
|
19
|
+
|
20
|
+
def initialize(logger: Logger.new($stdout))
|
21
|
+
@connection = create_amqp_connection
|
22
|
+
@channel = @connection.create_channel
|
23
|
+
@exchange = @channel.default_exchange
|
24
|
+
@logger = logger
|
25
|
+
end
|
26
|
+
|
27
|
+
def setup(agent_id:, logger:)
|
28
|
+
queue = create_queue(agent_id)
|
29
|
+
|
30
|
+
# Returning the queue to be used in the Base class
|
31
|
+
queue
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_queue(agent_id)
|
35
|
+
queue_name = "#{agent_id}"
|
36
|
+
@channel.queue(queue_name, expires: QUEUE_TTL)
|
37
|
+
end
|
38
|
+
|
39
|
+
def listen_for_messages(
|
40
|
+
queue,
|
41
|
+
request_handler:,
|
42
|
+
response_handler:,
|
43
|
+
control_handler:
|
44
|
+
)
|
45
|
+
queue.subscribe(block: true) do |delivery_info, properties, body|
|
46
|
+
message = JSON.parse(body, symbolize_names: true)
|
47
|
+
logger.debug "Received message: #{message.inspect}"
|
48
|
+
|
49
|
+
type = message.dig(:header, :type)
|
50
|
+
|
51
|
+
case type
|
52
|
+
when "request"
|
53
|
+
request_handler.call(message)
|
54
|
+
when "response"
|
55
|
+
response_handler.call(message)
|
56
|
+
when "control"
|
57
|
+
control_handler.call(message)
|
58
|
+
else
|
59
|
+
raise NotImplementedError, "Unsupported message type: #{type}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
def publish(message)
|
66
|
+
queue_name = message.dig(:header, :to_uuid)
|
67
|
+
|
68
|
+
begin
|
69
|
+
json_payload = JSON.generate(message)
|
70
|
+
|
71
|
+
exchange.publish(json_payload, routing_key: queue_name)
|
72
|
+
|
73
|
+
logger.info "Message published successfully to queue: #{queue_name}"
|
74
|
+
|
75
|
+
# Return a success status
|
76
|
+
{ success: true, message: "Message published successfully" }
|
77
|
+
|
78
|
+
rescue JSON::GeneratorError => e
|
79
|
+
logger.error "Failed to convert payload to JSON: #{e.message}"
|
80
|
+
{ success: false, error: "JSON conversion error: #{e.message}" }
|
81
|
+
|
82
|
+
rescue Bunny::ConnectionClosedError, Bunny::ChannelAlreadyClosed => e
|
83
|
+
logger.error "Failed to publish message: #{e.message}"
|
84
|
+
{ success: false, error: "Publishing error: #{e.message}" }
|
85
|
+
|
86
|
+
rescue StandardError => e
|
87
|
+
logger.error "Unexpected error while publishing message: #{e.message}"
|
88
|
+
{ success: false, error: "Unexpected error: #{e.message}" }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
def delete_queue(queue_name)
|
94
|
+
return logger.warn("Attempted to delete queue with nil name") if queue_name.nil?
|
95
|
+
|
96
|
+
begin
|
97
|
+
queue = @channel.queue(queue_name, passive: true)
|
98
|
+
queue.delete
|
99
|
+
logger.info "Queue #{queue_name} was deleted"
|
100
|
+
rescue Bunny::NotFound
|
101
|
+
logger.warn "Queue #{queue_name} not found"
|
102
|
+
rescue StandardError => e
|
103
|
+
logger.error "Error deleting queue #{queue_name}: #{e.message}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
################################################
|
109
|
+
private
|
110
|
+
|
111
|
+
def create_amqp_connection
|
112
|
+
Bunny.new.tap(&:start)
|
113
|
+
rescue Bunny::TCPConnectionFailed, StandardError => e
|
114
|
+
logger.error "Failed to connect to AMQP: #{e.message}"
|
115
|
+
raise "AMQP Connection Error: #{e.message}. Please check your AMQP server and try again."
|
116
|
+
end
|
117
|
+
end
|
data/lib/agent99/base.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# lib/agent99/base.rb
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'json'
|
5
|
+
require 'json_schema'
|
6
|
+
|
7
|
+
require_relative 'timestamp'
|
8
|
+
require_relative 'registry_client'
|
9
|
+
require_relative 'amqp_message_client'
|
10
|
+
require_relative 'nats_message_client'
|
11
|
+
|
12
|
+
require_relative 'header_management'
|
13
|
+
require_relative 'agent_discovery'
|
14
|
+
require_relative 'control_actions'
|
15
|
+
require_relative 'agent_lifecycle'
|
16
|
+
require_relative 'message_processing'
|
17
|
+
|
18
|
+
# The Agent99::Base class serves as the foundation for creating AI agents in a distributed system.
|
19
|
+
# It provides core functionality for agent registration, message handling, and communication.
|
20
|
+
#
|
21
|
+
# This class:
|
22
|
+
# - Manages agent registration and withdrawal
|
23
|
+
# - Handles incoming messages (requests, responses, and control messages)
|
24
|
+
# - Provides a framework for defining agent capabilities
|
25
|
+
# - Implements error handling and logging
|
26
|
+
# - Supports configuration updates and status reporting
|
27
|
+
#
|
28
|
+
# Subclasses should override specific methods like `receive_request`, `receive_response`,
|
29
|
+
# and `capabilities` to define custom behavior for different types of agents.
|
30
|
+
#
|
31
|
+
class Agent99::Base
|
32
|
+
include Agent99::HeaderManagement
|
33
|
+
include Agent99::AgentDiscovery
|
34
|
+
include Agent99::ControlActions
|
35
|
+
include Agent99::AgentLifecycle
|
36
|
+
include Agent99::MessageProcessing
|
37
|
+
|
38
|
+
MESSAGE_TYPES = %w[request response control]
|
39
|
+
|
40
|
+
CONTROL_HANDLERS = {
|
41
|
+
'shutdown' => :handle_shutdown,
|
42
|
+
'pause' => :handle_pause,
|
43
|
+
'resume' => :handle_resume,
|
44
|
+
'update_config' => :handle_update_config,
|
45
|
+
'status' => :handle_status_request
|
46
|
+
}
|
47
|
+
|
48
|
+
attr_reader :id, :capabilities, :name, :payload, :header, :logger, :queue
|
49
|
+
attr_accessor :registry_client, :message_client
|
50
|
+
|
51
|
+
|
52
|
+
###################################################
|
53
|
+
private
|
54
|
+
|
55
|
+
def handle_error(message, error)
|
56
|
+
logger.error "#{message}: #{error.message}"
|
57
|
+
logger.debug error.backtrace.join("\n")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# lib/agent99/control_actions.rb
|
2
|
+
|
3
|
+
|
4
|
+
module Agent99::ControlActions
|
5
|
+
|
6
|
+
|
7
|
+
################################################
|
8
|
+
private
|
9
|
+
|
10
|
+
# Handles the shutdown control message.
|
11
|
+
#
|
12
|
+
def handle_shutdown
|
13
|
+
logger.info "Received shutdown command. Initiating graceful shutdown..."
|
14
|
+
send_control_response("Shutting down")
|
15
|
+
fini
|
16
|
+
exit(0)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Handles the pause control message.
|
20
|
+
#
|
21
|
+
def handle_pause
|
22
|
+
@paused = true
|
23
|
+
logger.info "Agent paused"
|
24
|
+
send_control_response("Paused")
|
25
|
+
end
|
26
|
+
|
27
|
+
# Handles the resume control message.
|
28
|
+
#
|
29
|
+
def handle_resume
|
30
|
+
@paused = false
|
31
|
+
logger.info "Agent resumed"
|
32
|
+
send_control_response("Resumed")
|
33
|
+
end
|
34
|
+
|
35
|
+
# Handles the update_config control message.
|
36
|
+
#
|
37
|
+
def handle_update_config
|
38
|
+
new_config = payload[:config]
|
39
|
+
@config = new_config
|
40
|
+
logger.info "Configuration updated: #{@config}"
|
41
|
+
send_control_response("Configuration updated")
|
42
|
+
end
|
43
|
+
|
44
|
+
# Handles the status request control message.
|
45
|
+
#
|
46
|
+
def handle_status_request
|
47
|
+
status = {
|
48
|
+
id: @id,
|
49
|
+
name: @name,
|
50
|
+
paused: @paused,
|
51
|
+
config: @config,
|
52
|
+
uptime: (Time.now - @start_time).to_i
|
53
|
+
}
|
54
|
+
send_control_response("Status", status)
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# lib/agent99/header_management.rb
|
2
|
+
|
3
|
+
module Agent99::HeaderManagement
|
4
|
+
|
5
|
+
|
6
|
+
################################################
|
7
|
+
private
|
8
|
+
|
9
|
+
def header = @payload[:header]
|
10
|
+
def to_uuid = header[:to_uuid]
|
11
|
+
def from_uuid = header[:from_uuid]
|
12
|
+
def event_uuid = header[:event_uuid]
|
13
|
+
def timestamp = header[:timestamp]
|
14
|
+
def type = header[:type]
|
15
|
+
|
16
|
+
def return_address
|
17
|
+
header.merge(
|
18
|
+
to_uuid: from_uuid,
|
19
|
+
from_uuid: to_uuid,
|
20
|
+
timestamp: Agent99::Timestamp.new.to_i,
|
21
|
+
type: 'response'
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# experiments/agents/header_schema.rb
|
2
|
+
|
3
|
+
require 'simple_json_schema_builder'
|
4
|
+
|
5
|
+
require_relative 'timestamp'
|
6
|
+
|
7
|
+
class Agent99::HeaderSchema < SimpleJsonSchemaBuilder::Base
|
8
|
+
object do
|
9
|
+
string :from_uuid, required: true, examples: [SecureRandom.uuid]
|
10
|
+
string :to_uuid, required: true, examples: [SecureRandom.uuid]
|
11
|
+
string :event_uuid, required: true, examples: [SecureRandom.uuid]
|
12
|
+
string :type, required: true, examples: %w[request response control]
|
13
|
+
integer :timestamp, required: true, examples: [Agent99::Timestamp.new.to_i]
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# lib/agent99/message_processing.rb
|
2
|
+
|
3
|
+
module Agent99::MessageProcessing
|
4
|
+
|
5
|
+
|
6
|
+
# Starts the agent's main loop for processing messages.
|
7
|
+
#
|
8
|
+
def run
|
9
|
+
dispatcher
|
10
|
+
end
|
11
|
+
|
12
|
+
################################################
|
13
|
+
private
|
14
|
+
|
15
|
+
# Main message dispatching loop.
|
16
|
+
#
|
17
|
+
def dispatcher
|
18
|
+
@start_time = Time.now
|
19
|
+
@paused = false
|
20
|
+
@config = {}
|
21
|
+
|
22
|
+
message_client.listen_for_messages(
|
23
|
+
queue,
|
24
|
+
request_handler: ->(message) { process_request(message) unless paused? },
|
25
|
+
response_handler: ->(message) { process_response(message) unless paused? },
|
26
|
+
control_handler: ->(message) { process_control(message) }
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
# Processes incoming request messages.
|
32
|
+
#
|
33
|
+
# @param message [Hash] The incoming message
|
34
|
+
#
|
35
|
+
def process_request(message)
|
36
|
+
@payload = message
|
37
|
+
@header = payload[:header]
|
38
|
+
return unless validate_schema.empty?
|
39
|
+
receive_request
|
40
|
+
end
|
41
|
+
|
42
|
+
# Processes incoming response messages.
|
43
|
+
#
|
44
|
+
# @param message [Hash] The incoming message
|
45
|
+
#
|
46
|
+
def process_response(message)
|
47
|
+
@payload = message
|
48
|
+
receive_response
|
49
|
+
end
|
50
|
+
|
51
|
+
# Processes incoming control messages.
|
52
|
+
#
|
53
|
+
# @param message [Hash] The incoming message
|
54
|
+
#
|
55
|
+
def process_control(message)
|
56
|
+
@payload = message
|
57
|
+
receive_control
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
|
62
|
+
# Handles incoming request messages (to be overridden by subclasses).
|
63
|
+
#
|
64
|
+
def receive_request
|
65
|
+
logger.info "Received request: #{payload}"
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
# Handles incoming response messages (to be overridden by subclasses).
|
70
|
+
#
|
71
|
+
def receive_response
|
72
|
+
logger.info "Received response: #{payload}"
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
# Processes incoming control messages.
|
77
|
+
#
|
78
|
+
# @raise [StandardError] If there's an error processing the control message
|
79
|
+
#
|
80
|
+
def receive_control
|
81
|
+
action = payload[:action]
|
82
|
+
handler = CONTROL_HANDLERS[action]
|
83
|
+
|
84
|
+
if handler
|
85
|
+
send(handler)
|
86
|
+
else
|
87
|
+
logger.warn "Unknown control action: #{action}"
|
88
|
+
end
|
89
|
+
|
90
|
+
rescue StandardError => e
|
91
|
+
logger.error "Error processing control message: #{e.message}"
|
92
|
+
send_control_response("Error", { error: e.message })
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
# Sends a response message.
|
97
|
+
#
|
98
|
+
# @param response [Hash] The response to send
|
99
|
+
#
|
100
|
+
def send_response(response)
|
101
|
+
response[:header] = return_address
|
102
|
+
@message_client.publish(response)
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
# Sends a control response message.
|
107
|
+
#
|
108
|
+
# @param message [String] The response message
|
109
|
+
# @param data [Hash, nil] Additional data to include in the response
|
110
|
+
#
|
111
|
+
def send_control_response(message, data = nil)
|
112
|
+
response = {
|
113
|
+
header: return_address.merge(type: 'control'),
|
114
|
+
message: message,
|
115
|
+
data: data
|
116
|
+
}
|
117
|
+
@message_client.publish(response)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Validates the incoming message against the defined schema.
|
121
|
+
#
|
122
|
+
# @return [Array] An array of validation errors, empty if validation succeeds
|
123
|
+
#
|
124
|
+
def validate_schema
|
125
|
+
schema = JsonSchema.parse!(self.class::REQUEST_SCHEMA)
|
126
|
+
schema.expand_references!
|
127
|
+
validator = JsonSchema::Validator.new(schema)
|
128
|
+
|
129
|
+
validator.validate(@payload)
|
130
|
+
[]
|
131
|
+
|
132
|
+
rescue JsonSchema::ValidationError => e
|
133
|
+
handle_error("Validation error", e)
|
134
|
+
send_response(type: 'error', errors: e.messages)
|
135
|
+
e.messages
|
136
|
+
end
|
137
|
+
|
138
|
+
# Retrieves a field from the payload or returns a default value.
|
139
|
+
#
|
140
|
+
# @param field [Symbol] The field to retrieve
|
141
|
+
# @return [Object] The value of the field or its default
|
142
|
+
#
|
143
|
+
def get(field)
|
144
|
+
payload[field] || default(field)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Returns the default value for a field from the schema.
|
148
|
+
#
|
149
|
+
# @param field [Symbol] The field to get the default for
|
150
|
+
# @return [Object, nil] The default value or nil if not found
|
151
|
+
#
|
152
|
+
def default(field)
|
153
|
+
self.class::REQUEST_SCHEMA.dig(:properties, field, :examples)&.first
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# lib/agent99/nats_message_client.rb
|
2
|
+
|
3
|
+
require 'nats/client'
|
4
|
+
require 'json'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
class Agent99::NatsMessageClient
|
8
|
+
@instance = nil
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def instance
|
12
|
+
@instance ||= new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor :logger, :nats
|
17
|
+
|
18
|
+
def initialize(logger: Logger.new($stdout))
|
19
|
+
@nats = create_nats_connection
|
20
|
+
@logger = logger
|
21
|
+
end
|
22
|
+
|
23
|
+
def setup(agent_id:, logger:)
|
24
|
+
@logger = logger
|
25
|
+
# NATS doesn't require explicit queue creation, so we'll just return the agent_id
|
26
|
+
agent_id
|
27
|
+
end
|
28
|
+
|
29
|
+
def listen_for_messages(
|
30
|
+
queue,
|
31
|
+
request_handler:,
|
32
|
+
response_handler:,
|
33
|
+
control_handler:
|
34
|
+
)
|
35
|
+
@nats.subscribe(queue) do |msg|
|
36
|
+
message = JSON.parse(msg.data, symbolize_names: true)
|
37
|
+
logger.debug "Received message: #{message.inspect}"
|
38
|
+
|
39
|
+
type = message.dig(:header, :type)
|
40
|
+
|
41
|
+
case type
|
42
|
+
when "request"
|
43
|
+
request_handler.call(message)
|
44
|
+
when "response"
|
45
|
+
response_handler.call(message)
|
46
|
+
when "control"
|
47
|
+
control_handler.call(message)
|
48
|
+
else
|
49
|
+
raise NotImplementedError, "Unsupported message type: #{type}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Keep the connection open
|
54
|
+
loop { sleep 1 }
|
55
|
+
end
|
56
|
+
|
57
|
+
def publish(message)
|
58
|
+
queue_name = message.dig(:header, :to_uuid)
|
59
|
+
|
60
|
+
begin
|
61
|
+
json_payload = JSON.generate(message)
|
62
|
+
|
63
|
+
@nats.publish(queue_name, json_payload)
|
64
|
+
|
65
|
+
logger.info "Message published successfully to queue: #{queue_name}"
|
66
|
+
|
67
|
+
# Return a success status
|
68
|
+
{ success: true, message: "Message published successfully" }
|
69
|
+
|
70
|
+
rescue JSON::GeneratorError => e
|
71
|
+
logger.error "Failed to convert payload to JSON: #{e.message}"
|
72
|
+
{ success: false, error: "JSON conversion error: #{e.message}" }
|
73
|
+
|
74
|
+
rescue NATS::IO::TimeoutError => e
|
75
|
+
logger.error "Failed to publish message: #{e.message}"
|
76
|
+
{ success: false, error: "Publishing error: #{e.message}" }
|
77
|
+
|
78
|
+
rescue StandardError => e
|
79
|
+
logger.error "Unexpected error while publishing message: #{e.message}"
|
80
|
+
{ success: false, error: "Unexpected error: #{e.message}" }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def delete_queue(queue_name)
|
85
|
+
# NATS doesn't have the concept of deleting queues.
|
86
|
+
# Subjects are automatically cleaned up when there are no more subscribers.
|
87
|
+
logger.info "NATS doesn't require explicit queue deletion. Subject #{queue_name} will be automatically cleaned up."
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
################################################
|
92
|
+
private
|
93
|
+
|
94
|
+
def create_nats_connection
|
95
|
+
NATS.connect
|
96
|
+
rescue NATS::IO::ConnectError => e
|
97
|
+
logger.error "Failed to connect to NATS: #{e.message}"
|
98
|
+
raise "NATS Connection Error: #{e.message}. Please check your NATS server and try again."
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# experiments/agents/agent99/registry_client.rb
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'net/http'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
class Agent99::RegistryClient
|
8
|
+
attr_accessor :logger
|
9
|
+
|
10
|
+
def initialize(
|
11
|
+
base_url: ENV.fetch('REGISTRY_BASE_URL', 'http://localhost:4567'),
|
12
|
+
logger: Logger.new($stdout)
|
13
|
+
)
|
14
|
+
@base_url = base_url
|
15
|
+
@logger = logger
|
16
|
+
@http_client = Net::HTTP.new(URI.parse(base_url).host, URI.parse(base_url).port)
|
17
|
+
end
|
18
|
+
|
19
|
+
def register(name:, capabilities:)
|
20
|
+
request = create_request(:post, "/register", { name: name, capabilities: capabilities })
|
21
|
+
@id = send_request(request)
|
22
|
+
end
|
23
|
+
|
24
|
+
def withdraw(id)
|
25
|
+
return logger.warn("Agent not registered") unless id
|
26
|
+
|
27
|
+
request = create_request(:delete, "/withdraw/#{id}")
|
28
|
+
send_request(request)
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def discover(capability:)
|
33
|
+
encoded_capability = URI.encode_www_form_component(capability)
|
34
|
+
request = create_request(:get, "/discover?capability=#{encoded_capability}")
|
35
|
+
send_request(request)
|
36
|
+
end
|
37
|
+
|
38
|
+
################################################
|
39
|
+
private
|
40
|
+
|
41
|
+
def create_request(method, path, body = nil)
|
42
|
+
request = Object.const_get("Net::HTTP::#{method.capitalize}").new(path, { "Content-Type" => "application/json" })
|
43
|
+
request.body = body.to_json if body
|
44
|
+
request
|
45
|
+
end
|
46
|
+
|
47
|
+
def send_request(request)
|
48
|
+
response = @http_client.request(request)
|
49
|
+
|
50
|
+
handle_response(response)
|
51
|
+
rescue JSON::ParserError => e
|
52
|
+
logger.error "JSON parsing error: #{e.message}"
|
53
|
+
nil
|
54
|
+
rescue StandardError => e
|
55
|
+
logger.error "Request error: #{e.message}"
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def handle_response(response)
|
60
|
+
case response
|
61
|
+
when Net::HTTPOK
|
62
|
+
JSON.parse(response.body, symbolize_names: true)
|
63
|
+
when Net::HTTPCreated
|
64
|
+
JSON.parse(response.body, symbolize_names: true)[:uuid]
|
65
|
+
when Net::HTTPNoContent
|
66
|
+
logger.info "Action completed successfully."
|
67
|
+
nil
|
68
|
+
else
|
69
|
+
logger.error "Error: #{JSON.parse(response.body, symbolize_names: true)[:error]}"
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|