agent99 0.0.1
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/.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
|