agent99 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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