agent99 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 827e521c4a6580cfe954cf07cf04f1a822c00b9ed6eebf75598d925aeb555626
4
- data.tar.gz: 1b3e9b499610b9b3853757432fd4e3e39acab8fb70e29feb94eae1d22f6097aa
3
+ metadata.gz: eeb28a9ea8730c5ff33d5722f4dad0d0bcec6a54d58784203d1c147b64b17223
4
+ data.tar.gz: c27cb24f7ee720f758b94a32bd5d33e1ea5a92dc490022122cc40eaaa736620e
5
5
  SHA512:
6
- metadata.gz: ce549f64fb88cd2f152e7af27b9724dbfedfda0f6255033ea1773dda7eaf89dd71a6b622667d023b3093ae1eca72ae3a55e7bd6b1f538e3035fe889ab52ac7c4
7
- data.tar.gz: 959b0c9c342738bb9ee75f2dd82a4caaa825a13656f6d12dba2a11434857c08f11d746c1754f03ce1902f9e4ced1f3b7e1067ab1cdbe39c054250b23592ae94f
6
+ metadata.gz: dc796885c649aa669f77954eb25c5a9e8667d5aff6958054b53ca589fd3e620bb03ba3fce5a4b707bc6d2bd67ee2672291d1e4a0e0ff32b5d1d06912766492b0
7
+ data.tar.gz: f01c4e6d5a3353c722f87c965faaa6abede17ed57861a9b4fc82829e0c5a33d4bb483eb47420b9cef22bd697fb17d2e7e437bf9a2e5314fc07ab208bfb9d374d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - 2024-11-28
3
+
4
+ ## Released
5
+
6
+ ### [0.0.2] - 2024-12-07
7
+
8
+ - Added examples/control.rb
9
+ - request status from all agents
10
+ - other control messages not worked on yet
11
+
12
+ ### [0.0.1] - 2024-11-28
4
13
 
5
14
  - Initial release
data/README.md CHANGED
@@ -4,6 +4,48 @@
4
4
 
5
5
  Agent99 is a Ruby-based framework for building and managing AI agents in a distributed system. It provides a robust foundation for creating intelligent agents that can communicate, discover each other, and perform various tasks.
6
6
 
7
+ ## Hype!
8
+
9
+ 🌟 **Introducing Agent99**: Your Friendly Ruby Sidekick for Building Intelligent Software Agents! 🌟
10
+
11
+ Do you ever miss the charm and wit of Barbara Feldon's iconic character from *Get Smart*? Just as Agent 99 was always ready to tackle mischief and save the day, **Agent99** is here to help you create your very own software agents that can communicate, collaborate, and conquer tasks in a smart and efficient way!
12
+
13
+ ### Why Choose Agent99?
14
+
15
+ 🔍 **Independent Agents, Unified Communication**: Say goodbye to complex service orchestration! Agent99 empowers you to build micro-services that communicate seamlessly through a robust peer-to-peer messaging network. Each agent operates independently while working together like a well-oiled machine!
16
+
17
+ 🤖 **Smart Automation for the Modern Age**: In the era of AI, why settle for less? With Agent99, you can develop software agents that respond intelligently to incoming requests—just like a secret agent reacting to their next mission! Deploy your own digital spies to gather data, complete tasks, and more.
18
+
19
+ 📦 **Quick Setup & Easy Integration**: Just like Agent 99’s quick thinking, our library is designed for rapid development. Get up and running in no time, whether you’re building a small project or an enterprise-level solution!
20
+
21
+
22
+ ### Features that Make Agent99 a Must-Have:
23
+
24
+ - 🌐 **Peer-to-Peer Messaging**: Ensure that your agents can communicate effortlessly, just like Agent 99 and Max.
25
+ - 🚀 **Agile Development**: Design and deploy agents at lightning speed, giving you the freedom to innovate.
26
+ - 🔒 **Secure Communication**: Built with security in mind, because even our agents deserve to keep their secrets safe.
27
+ - 📊 **Scalability**: Expand your network of agents as your needs grow—no mission is too big!
28
+
29
+ ### Get Smart! Join the Revolution!
30
+
31
+ Whether you’re a seasoned Ruby developer or just getting started, Agent99 provides the tools you need to build your very own quirky, intelligent agents. Just like Agent 99, your agents will be clever, adaptable, and ready to tackle any challenge!
32
+
33
+ ### How to Get Started:
34
+
35
+ 1. **Install**: Simply add Agent99 to your Gemfile and run `bundle install`.
36
+ 2. **Create an Agent**: Use our simple API to define and deploy your first agent.
37
+ 3. **Watch Them Work**: Sit back and relax as your agents spring into action, communicating with one another to accomplish tasks that would impress even the chief of CONTROL!
38
+
39
+ ### Spread the Word!
40
+
41
+ **Join the Agent99 community** on GitHub and share your experiences, tips, and feedback. Let’s build a world of smart agents together! And remember, just like Agent 99 always had Max’s back, we’ve got yours during your development journey.
42
+
43
+ 🕵️‍♂️ **Become an Agent!** Dive into the world of Agent99 today: [GitHub Repository](#) 📖
44
+
45
+ **Agent99** – Because when it comes to building software agents, it's all about being smart!
46
+
47
+ ... end of the Hype; now back to normal.
48
+
7
49
  ## Features
8
50
 
9
51
  - Agent Lifecycle Management: Easy setup and teardown of agents
data/examples/README.md CHANGED
@@ -1,41 +1,36 @@
1
- # Agent99 Framework Examples
2
-
3
- - TODO
4
- - Review and edit
5
- - Add instructions for brew install rabbitmq nats-server boxes
6
- - review example agents to see code can be tightened
1
+ # Agent99 Framework Examples - 86 and the Chief
7
2
 
8
3
  This folder contains example implementations using the Agent99 framework. The framework provides a foundation for building AI agents that can communicate with each other through a message-based system.
9
4
 
10
5
  ## Files
11
6
 
12
- ### 1. hello_world.rb
7
+ ### 1. maxwell_agent86.rb
13
8
 
14
- This file demonstrates a basic AI agent implementation using the Agent99 framework.
9
+ This file demonstrates a basic agent implementation using the Agent99 framework.
15
10
 
16
- - Class: `HelloWorld < Agent99::Base`
11
+ - Class: `MaxwellAgent86 < Agent99::Base`
17
12
  - Functionality: Responds to "hello world" requests
18
13
  - Key methods:
19
14
  - `receive_request`: Handles incoming requests
20
15
  - `validate_request`: Validates the request against a schema
21
16
  - `process`: Generates the response
22
17
 
23
- ### 2. hello_world_client.rb
18
+ ### 2. chief_agent.rb
24
19
 
25
- This file shows how to create a client that interacts with the HelloWorld agent.
20
+ This file shows how to create a client that interacts with the MaxwellAgent86 agent.
26
21
 
27
- - Class: `HelloWorldClient < Agent99::Base`
28
- - Functionality: Sends a request to a HelloWorld agent and processes the response
22
+ - Class: `ChiefAgent < Agent99::Base`
23
+ - Functionality: Sends a request to an agent who can be a greeter and processes the response
29
24
  - Key methods:
30
25
  - `init`: Initiates the request sending process
31
26
  - `send_request`: Builds and sends the request
32
27
  - `receive_response`: Handles the response from the HelloWorld agent
33
28
 
34
- ### 3. hello_world_request.rb
29
+ ### 3. mexwell_request.rb
35
30
 
36
- This file defines the schema for HelloWorld requests using SimpleJsonSchemaBuilder.
31
+ This file defines the schema for MaxwellAgent86 requests using SimpleJsonSchemaBuilder.
37
32
 
38
- - Class: `HelloWorldRequest < SimpleJsonSchemaBuilder::Base`
33
+ - Class: `MaxwellRequest < SimpleJsonSchemaBuilder::Base`
39
34
  - Defines the structure of a valid HelloWorld request
40
35
 
41
36
  ### 4. registry.rb
@@ -52,28 +47,20 @@ This file implements a simple registry service for AI agents using Sinatra.
52
47
 
53
48
  ## Usage
54
49
 
55
- 1. Start the registry service:
56
- ```
57
- ruby registry.rb
58
- ```
50
+ From the examples directory you will need to start three different processes. You will want to keep them all in the forgound so it would be best to start them in different terminal windows.
51
+
52
+ Start the sample registry first: `./registry.rb`
53
+
54
+ Then start the service agent: `./maxwell_agent86.rb`
55
+ Maxwell will will register itself, get its UUID and setup a message queue to which it will listen for its service requests.
59
56
 
60
- 2. Run the HelloWorld agent:
61
- ```
62
- ruby hello_world.rb
63
- ```
57
+ Finally start the chief agent in charge: `./chief_agent.rb`
58
+ The Chief also registers itself but no other agent can give the Chief missions. That's his job. The chief gets his mission UUID, sets up a message queue to listen for the reports from the field after he sends out his request (aka order)
64
59
 
65
- 3. Run the HelloWorld client:
66
- ```
67
- ruby hello_world_client.rb
68
- ```
60
+ But first the Chief asks the registry for the UUIDs of all agents who can handle a "greeter" request. The Chief selects one of those agents and sends the agent a greet request. The Chief then waits for a response to the request. When it comes in, the chiefs displays the response and terminates.
69
61
 
70
- ## Dependencies
62
+ Run the chief a few times in a roll. Some times the agent to whom the Chief issues his requests does not always respond the you would expect.
71
63
 
72
- - Ruby 3.3+
73
- - Gems: json, json_schema, sinatra, bunny, securerandom
64
+ ![Agent99 Framework Diagram](diagram.png)
74
65
 
75
- ## Notes
76
66
 
77
- - The framework uses modern Ruby 3.3 syntax, especially for hashes and method signatures with named parameters.
78
- - The examples demonstrate basic usage of the Agent99 framework, including request/response handling, validation, and agent discovery.
79
- - The registry service uses an in-memory store (AGENT_REGISTRY) for simplicity, but it's recommended to use a more robust solution like SQLite for production use.
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env ruby
2
+ # examples/chief_agent.rb
3
+ #
4
+ # This program is a type :client agent.
5
+ #
6
+ # This program calls a CLI program named boxes to highlight
7
+ # the response received to the request that it sent.
8
+ #
9
+ # brew install boxes
10
+ #
11
+ # Run this program several times to see if Maxwell Agent86
12
+ # messes up his mission which he is prone to do half the time.
13
+
14
+ require_relative '../lib/agent99'
15
+
16
+ class ChiefAgent < Agent99::Base
17
+ TYPE = :client
18
+
19
+ # init is called at the end of the initialization process.
20
+ # It may be only something that a :client type agent would do.
21
+ #
22
+ # For this client it sends out a request as its first order of
23
+ # business and expects to receive a response.
24
+ #
25
+ def init
26
+ action = 'greeter'
27
+ agent = discover_agent(
28
+ capability: action,
29
+ how_many: 1
30
+ ).first[:uuid]
31
+
32
+ send_request(agent:)
33
+
34
+ rescue Exception => e
35
+ logger.warn "No Agents are available as #{action}"
36
+ exit(1)
37
+ end
38
+
39
+
40
+ ##################################################
41
+ private
42
+
43
+ def send_request(agent:)
44
+ request = build_request(
45
+ to_uuid: agent,
46
+ greeting: 'Hey',
47
+ name: 'MadBomber'
48
+ )
49
+
50
+ result = @message_client.publish(request)
51
+ logger.info "Sent request: #{request.inspect}; status? #{result.inspect}"
52
+ end
53
+
54
+
55
+ def build_request(
56
+ to_uuid:,
57
+ greeting: 'Hello',
58
+ name: 'World'
59
+ )
60
+
61
+ {
62
+ header: {
63
+ type: 'request',
64
+ from_uuid: @id,
65
+ to_uuid: ,
66
+ event_uuid: SecureRandom.uuid,
67
+ timestamp: Agent99::Timestamp.new.to_i
68
+ },
69
+ greeting:,
70
+ name:
71
+ }
72
+ end
73
+
74
+
75
+ def receive_response
76
+ logger.info "Received response: #{payload.inspect}"
77
+ result = payload[:result]
78
+
79
+ puts
80
+ puts `echo "#{result}" | boxes -d info`
81
+ puts
82
+
83
+ exit(0)
84
+ end
85
+
86
+
87
+ def capabilities
88
+ ['Chief of Control']
89
+ end
90
+ end
91
+
92
+
93
+ # Example usage
94
+ client = ChiefAgent.new
95
+ client.run
96
+
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env ruby
2
+ # control.rb
3
+
4
+ require_relative '../lib/agent99'
5
+
6
+ class Control < Agent99::Base
7
+ TYPE = :hybrid
8
+
9
+ attr_accessor :statuses
10
+
11
+ def init
12
+ @agents = @registry_client.fetch_all_agents
13
+ @statuses = {}
14
+ end
15
+
16
+
17
+ def capabilities
18
+ ['control', 'headquarters', 'secret underground base']
19
+ end
20
+
21
+
22
+ def send_control_message(message:, payload: {})
23
+ @agents.each do |agent|
24
+ response = @message_client.publish(
25
+ header: {
26
+ to_uuid: agent[:uuid],
27
+ from_uuid: @id,
28
+ event_uuid: SecureRandom.uuid,
29
+ type: 'control',
30
+ timestamp: Agent99::Timestamp.new.to_i
31
+ },
32
+ action: message,
33
+ payload: payload
34
+ )
35
+ puts "Sent #{message} to #{agent[:name]}: #{response[:success] ? 'Success' : 'Failed'}"
36
+ end
37
+ end
38
+
39
+
40
+ def pause_all
41
+ send_control_message(message: 'pause')
42
+ end
43
+
44
+
45
+ def resume_all
46
+ send_control_message(message: 'resume')
47
+ end
48
+
49
+
50
+ def stop_all
51
+ send_control_message(message: 'stop')
52
+ end
53
+
54
+ def get_all_status
55
+ @statuses.clear # Reset statuses before new request
56
+
57
+ @agents.each do |agent|
58
+ @message_client.publish(
59
+ header: {
60
+ to_uuid: agent[:uuid],
61
+ from_uuid: @id,
62
+ event_uuid: SecureRandom.uuid,
63
+ type: 'control',
64
+ timestamp: Agent99::Timestamp.new.to_i
65
+ },
66
+ action: 'status'
67
+ )
68
+ end
69
+
70
+ # Wait for responses (with timeout)
71
+ sleep 2 # Give agents time to respond
72
+ @statuses
73
+ end
74
+
75
+ def receive_response
76
+ if payload[:action] == 'response' && payload[:data][:type] == 'status'
77
+ agent_name = payload[:header][:from_uuid]
78
+ @statuses[agent_name] = payload[:data]
79
+ logger.info "Received status from #{agent_name}: #{payload[:data]}"
80
+ elsif payload[:action] == 'status' && payload[:header][:from_uuid] == @id
81
+ # Handle our own status request
82
+ handle_status_request
83
+ end
84
+ end
85
+
86
+
87
+ end
88
+
89
+
90
+ if __FILE__ == $PROGRAM_NAME
91
+ control = Control.new
92
+
93
+ # Start the message processing in a separate thread
94
+ dispatcher_thread = Thread.new do
95
+ control.run
96
+ end
97
+
98
+ # UI thread
99
+ begin
100
+ loop do
101
+ puts "\n1. Pause all agents"
102
+ puts "2. Resume all agents"
103
+ puts "3. Stop all agents"
104
+ puts "4. Get all agents status"
105
+ puts "5. Exit"
106
+ print "\nEnter your choice: "
107
+
108
+ choice = gets.chomp.to_i
109
+
110
+ case choice
111
+ when 1
112
+ control.pause_all
113
+ when 2
114
+ control.resume_all
115
+ when 3
116
+ control.stop_all
117
+ when 4
118
+ statuses = control.get_all_status
119
+ sleep 2 # Give time for responses to arrive
120
+ puts JSON.pretty_generate(control.statuses)
121
+ when 5
122
+ puts "Exiting..."
123
+ Thread.exit
124
+ break
125
+ else
126
+ puts "Invalid choice. Please try again."
127
+ end
128
+ end
129
+ rescue Interrupt
130
+ puts "\nShutting down..."
131
+ ensure
132
+ dispatcher_thread.exit
133
+ control.fini
134
+ end
135
+ end
136
+
@@ -0,0 +1,22 @@
1
+ digraph agent_interaction {
2
+ rankdir=LR; // Left to right layout
3
+
4
+ // Define nodes for each component
5
+ node [shape=box];
6
+ ChiefAgent [label="ChiefAgent"];
7
+ MaxwellAgent86 [label="MaxwellAgent86"];
8
+ Registry [label="Registry Service"];
9
+
10
+ // Define edges for interactions
11
+ ChiefAgent -> Registry [label="GET /discover (greeter)"];
12
+ Registry -> ChiefAgent [label="Agent UUIDs"];
13
+
14
+ ChiefAgent -> MaxwellAgent86 [label="Send greet request"];
15
+ MaxwellAgent86 -> ChiefAgent [label="Response (Hello World)"];
16
+
17
+ MaxwellAgent86 -> Registry [label="POST /register"];
18
+ Registry -> MaxwellAgent86 [label="ACK (registration)"];
19
+
20
+ ChiefAgent -> Registry [label="POST /register"];
21
+ Registry -> ChiefAgent [label="ACK (registration)"];
22
+ }
Binary file
@@ -1,22 +1,31 @@
1
1
  #!/usr/bin/env ruby
2
- # examples/hello_world.rb
2
+ # examples/maxwell_agent86.rb
3
3
 
4
- require 'json'
5
- require 'json_schema'
4
+ # There are three types of agents: Server, Client and Hybrid.
5
+ # A Server receives a requests and _may_ send a response.
6
+ # A Client sends a request and _may_ expect a response.
7
+ # A Hybrid _may_ act like a Server or a Client
6
8
 
7
9
  require_relative '../lib/agent99'
8
- require_relative 'hello_world_request'
10
+ require_relative 'maxwell_request'
11
+
12
+ class MaxwellAgent86 < Agent99::Base
13
+ REQUEST_SCHEMA = MaxwellRequest.schema
14
+ TYPE = :server
9
15
 
10
- class HelloWorld < Agent99::Base
11
- REQUEST_SCHEMA = HelloWorldRequest.schema
12
16
  # RESPONSE_SCHEMA = Agent99::RESPONSE.schema
13
17
  # ERROR_SCHEMA = Agent99::ERROR.schema
14
18
 
19
+
20
+ #######################################
21
+ private
22
+
15
23
  # The request is in @payload
16
24
  def receive_request
17
25
  send_response( validate_request || process )
18
26
  end
19
27
 
28
+
20
29
  # This method validates the incoming request and returns any errors found
21
30
  # or nil if there are no errors.
22
31
  # It allows for returning an array of errors.
@@ -44,6 +53,7 @@ class HelloWorld < Agent99::Base
44
53
  }
45
54
  end
46
55
 
56
+
47
57
  # Validate the incoming request body against the schema
48
58
  validation_errors = validate_schema
49
59
  unless validation_errors.empty?
@@ -57,6 +67,7 @@ class HelloWorld < Agent99::Base
57
67
  responses.empty? ? nil : responses
58
68
  end
59
69
 
70
+
60
71
  # Returns the response value
61
72
  # All response message have the same schema in that
62
73
  # they have a header (all messages have headers) and
@@ -64,9 +75,13 @@ class HelloWorld < Agent99::Base
64
75
  # a JSON string, sure but then we would need a
65
76
  # RESPONSE_SCHEMA constant for the class.
66
77
  def process
67
- {
68
- result: get(:greeting) + ' ' + get(:name)
69
- }
78
+ result = if 50 <= rand(100)
79
+ get(:greeting) + ' ' + get(:name)
80
+ else
81
+ 'Missed it by that >< much.'
82
+ end
83
+
84
+ { result: result }
70
85
  end
71
86
 
72
87
 
@@ -76,16 +91,21 @@ class HelloWorld < Agent99::Base
76
91
  loger.warn("Unexpected response type message: response.inspect")
77
92
  end
78
93
 
79
- private
80
94
 
81
- # NOTE: what I'm thinking about here is similar to the
82
- # prompt tool (aka function) callback facility
83
- # where descriptive text is used to describe
84
- # what the tool does.
95
+ # Phase One Implementation is to do a search
96
+ # using the String#include? and the Array#include?
97
+ # methods. If you want discrete word-based selection
98
+ # then use an Array of Strings to define the different
99
+ # things this agent can do.
100
+ #
101
+ # If you want to match on sub-strings then define the
102
+ # the capabilities as a String.
103
+ #
104
+ # Subsequent implementations may use a semantic search
105
+ # to find the agents to use in which case capabilities may
106
+ # be constrained to be a String.
85
107
  #
86
- # TODO: scale this idea back to just keywords
87
- # until the registry program gets more
88
- # stuff added to its discovery process.
108
+ # For now, lets just go with the Array of Strings.
89
109
  #
90
110
  def capabilities
91
111
  %w[ greeter hello_world hello-world hello]
@@ -93,5 +113,5 @@ class HelloWorld < Agent99::Base
93
113
  end
94
114
 
95
115
  # Example usage
96
- agent = HelloWorld.new
116
+ agent = MaxwellAgent86.new
97
117
  agent.run # Starts listening for messages
@@ -1,8 +1,8 @@
1
- # examples/hello_world_request.rb
1
+ # examples/maxwell_request.rb
2
2
 
3
3
  require_relative '../lib/agent99/header_schema'
4
4
 
5
- class HelloWorldRequest < SimpleJsonSchemaBuilder::Base
5
+ class MaxwellRequest < SimpleJsonSchemaBuilder::Base
6
6
  object do
7
7
  object :header, schema: Agent99::HeaderSchema
8
8
 
data/examples/registry.rb CHANGED
@@ -6,7 +6,6 @@ include DebugMe
6
6
 
7
7
  require 'sinatra'
8
8
  require 'json'
9
- require 'bunny'
10
9
  require 'securerandom'
11
10
 
12
11
  # In-memory registry to store agent capabilities
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ # examples/start_rabbitmq_and_registry.sh
3
+ #
4
+ # brew install rabbitmq-server
5
+ #
6
+ # Start up the AMQP message broker (RabbitM@) in the background
7
+
8
+ echo "Starting rabbitmq-server in background ..."
9
+ rabbitmq-server &
10
+ sleep 2
11
+ open http://localhost:15672/#/queues
12
+
13
+ echo
14
+ echo
15
+ echo "Starting example registry in forground ..."
16
+ echo "http://localhost:4567"
17
+ echo
18
+ ruby registry.rb # blocks until control-C
19
+
20
+ # You may have to do control-c twice to stop both
21
+
22
+ echo "#"
23
+ echo "##"
24
+ echo "###"
25
+ echo "####"
26
+ echo "#####"
27
+ echo "registry is stopped"
28
+ echo "stopping the rabbitmq server ...."
29
+ rabbitmqctl stop
@@ -48,9 +48,21 @@ module Agent99::AgentLifecycle
48
48
  end
49
49
 
50
50
 
51
+ # Performs cleanup operations when the agent is shutting down.
52
+ #
53
+ def fini
54
+ if id
55
+ queue_name = id
56
+ withdraw
57
+ @message_client&.delete_queue(queue_name)
58
+ else
59
+ logger.warn('fini called with a nil id')
60
+ end
61
+ end
62
+
51
63
  ################################################
52
64
  private
53
-
65
+
54
66
  # Checks if the agent is currently paused.
55
67
  #
56
68
  # @return [Boolean] True if the agent is paused, false otherwise
@@ -71,17 +83,4 @@ module Agent99::AgentLifecycle
71
83
  end
72
84
  end
73
85
  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
86
+ end
@@ -6,6 +6,19 @@ require 'json_schema'
6
6
  require 'logger'
7
7
 
8
8
  class Agent99::AmqpMessageClient
9
+ CONFIG = {
10
+ host: "127.0.0.1",
11
+ port: 5672,
12
+ ssl: false,
13
+ vhost: "/",
14
+ user: "guest",
15
+ pass: "guest",
16
+ heartbeat: :server, # will use RabbitMQ setting
17
+ frame_max: 131072,
18
+ auth_mechanism: "PLAIN"
19
+ }
20
+
21
+
9
22
  QUEUE_TTL = 60_000 # 60 seconds TTL
10
23
  @instance = nil
11
24
 
@@ -17,16 +30,21 @@ class Agent99::AmqpMessageClient
17
30
 
18
31
  attr_accessor :logger, :channel, :exchange
19
32
 
20
- def initialize(logger: Logger.new($stdout))
33
+ def initialize(
34
+ config: CONFIG,
35
+ logger: Logger.new($stdout))
36
+ @config = config
21
37
  @connection = create_amqp_connection
22
- @channel = @connection.create_channel
23
- @exchange = @channel.default_exchange
24
- @logger = logger
38
+ @channel = @connection.create_channel
39
+ @exchange = @channel.default_exchange
40
+ @logger = logger
25
41
  end
26
42
 
27
43
  def setup(agent_id:, logger:)
28
44
  queue = create_queue(agent_id)
29
45
 
46
+ logger.info "Created queue for agent_id: #{agent_id}"
47
+
30
48
  # Returning the queue to be used in the Base class
31
49
  queue
32
50
  end
@@ -66,26 +84,39 @@ class Agent99::AmqpMessageClient
66
84
  queue_name = message.dig(:header, :to_uuid)
67
85
 
68
86
  begin
87
+ # FIXME: message.to_json
69
88
  json_payload = JSON.generate(message)
70
89
 
71
90
  exchange.publish(json_payload, routing_key: queue_name)
72
91
 
73
- logger.info "Message published successfully to queue: #{queue_name}"
92
+ logger.info "#{message.dig(:header,:type).to_s.upcase} message published successfully to queue: #{queue_name}"
74
93
 
75
94
  # Return a success status
76
- { success: true, message: "Message published successfully" }
95
+ {
96
+ success: true,
97
+ message: "Message published successfully"
98
+ }
77
99
 
78
100
  rescue JSON::GeneratorError => e
79
101
  logger.error "Failed to convert payload to JSON: #{e.message}"
80
- { success: false, error: "JSON conversion error: #{e.message}" }
102
+ {
103
+ success: false,
104
+ error: "JSON conversion error: #{e.message}"
105
+ }
81
106
 
82
107
  rescue Bunny::ConnectionClosedError, Bunny::ChannelAlreadyClosed => e
83
108
  logger.error "Failed to publish message: #{e.message}"
84
- { success: false, error: "Publishing error: #{e.message}" }
109
+ {
110
+ success: false,
111
+ error: "Publishing error: #{e.message}"
112
+ }
85
113
 
86
114
  rescue StandardError => e
87
115
  logger.error "Unexpected error while publishing message: #{e.message}"
88
- { success: false, error: "Unexpected error: #{e.message}" }
116
+ {
117
+ success: false,
118
+ error: "Unexpected error: #{e.message}"
119
+ }
89
120
  end
90
121
  end
91
122
 
@@ -109,7 +140,7 @@ class Agent99::AmqpMessageClient
109
140
  private
110
141
 
111
142
  def create_amqp_connection
112
- Bunny.new.tap(&:start)
143
+ Bunny.new(@config).tap(&:start)
113
144
  rescue Bunny::TCPConnectionFailed, StandardError => e
114
145
  logger.error "Failed to connect to AMQP: #{e.message}"
115
146
  raise "AMQP Connection Error: #{e.message}. Please check your AMQP server and try again."
data/lib/agent99/base.rb CHANGED
@@ -36,14 +36,6 @@ class Agent99::Base
36
36
  include Agent99::MessageProcessing
37
37
 
38
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
39
 
48
40
  attr_reader :id, :capabilities, :name, :payload, :header, :logger, :queue
49
41
  attr_accessor :registry_client, :message_client
@@ -56,6 +48,12 @@ class Agent99::Base
56
48
  logger.error "#{message}: #{error.message}"
57
49
  logger.debug error.backtrace.join("\n")
58
50
  end
51
+
52
+
53
+ # the final rescue block
54
+ rescue StandardError => e
55
+ handle_error("Unhandled error in Agent99::Base", e)
56
+ exit(2)
59
57
  end
60
58
 
61
59
 
@@ -1,56 +1,89 @@
1
1
  # lib/agent99/control_actions.rb
2
2
 
3
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
4
+ module Agent99
5
+ CONTROL_HANDLERS = {
6
+ 'shutdown' => :handle_shutdown,
7
+ 'pause' => :handle_pause,
8
+ 'resume' => :handle_resume,
9
+ 'update_config' => :handle_update_config,
10
+ 'status' => :handle_status_request,
11
+ 'response' => :handle_control_response,
12
+ }
18
13
 
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
14
+ module ControlActions
26
15
 
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
16
 
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
17
+ ################################################
18
+ private
19
+
20
+ def handle_control_response
21
+ logger.info "Received control response: #{payload}"
22
+ response_type = payload.dig(:data, :type)
23
+ response_data = payload[:data]
24
+
25
+ case response_type
26
+ when 'status'
27
+ logger.info "Status update from agent: #{response_data}"
28
+ when 'error'
29
+ logger.error "Error from agent: #{response_data[:error]}"
30
+ else
31
+ logger.info "Generic control response: #{payload[:message]}"
32
+ end
33
+ end
34
+
35
+
36
+ # Handles the shutdown control message.
37
+ #
38
+ def handle_shutdown
39
+ logger.info "Received shutdown command. Initiating graceful shutdown..."
40
+ send_control_response("Shutting down")
41
+ fini
42
+ exit(0)
43
+ end
44
+
43
45
 
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)
46
+ # Handles the pause control message.
47
+ #
48
+ def handle_pause
49
+ @paused = true
50
+ logger.info "Agent paused"
51
+ send_control_response("Paused")
52
+ end
53
+
54
+
55
+ # Handles the resume control message.
56
+ #
57
+ def handle_resume
58
+ @paused = false
59
+ logger.info "Agent resumed"
60
+ send_control_response("Resumed")
61
+ end
62
+
63
+
64
+ # Handles the update_config control message.
65
+ #
66
+ def handle_update_config
67
+ new_config = payload[:config]
68
+ @config = new_config
69
+ logger.info "Configuration updated: #{@config}"
70
+ send_control_response("Configuration updated")
71
+ end
72
+
73
+
74
+ # Handles the status request control message.
75
+ #
76
+ def handle_status_request
77
+ status = {
78
+ type: 'status',
79
+ id: @id,
80
+ name: @name,
81
+ paused: @paused,
82
+ config: @config,
83
+ uptime: (Time.now - @start_time).to_i
84
+ }
85
+ send_control_response(status)
86
+ end
55
87
  end
56
88
  end
89
+
@@ -4,7 +4,7 @@ module Agent99::HeaderManagement
4
4
 
5
5
 
6
6
  ################################################
7
- private
7
+ # private
8
8
 
9
9
  def header = @payload[:header]
10
10
  def to_uuid = header[:to_uuid]
@@ -14,9 +14,11 @@ module Agent99::HeaderManagement
14
14
  def type = header[:type]
15
15
 
16
16
  def return_address
17
- header.merge(
18
- to_uuid: from_uuid,
19
- from_uuid: to_uuid,
17
+ return_address = payload[:header].dup
18
+
19
+ return_address.merge(
20
+ to_uuid: return_address[:from_uuid],
21
+ from_uuid: return_address[:to_uuid],
20
22
  timestamp: Agent99::Timestamp.new.to_i,
21
23
  type: 'response'
22
24
  )
@@ -16,14 +16,15 @@ module Agent99::MessageProcessing
16
16
  #
17
17
  def dispatcher
18
18
  @start_time = Time.now
19
- @paused = false
20
- @config = {}
19
+ @paused = false
20
+ @config = {}
21
+
21
22
 
22
23
  message_client.listen_for_messages(
23
24
  queue,
24
- request_handler: ->(message) { process_request(message) unless paused? },
25
+ request_handler: ->(message) { process_request(message) unless paused? },
25
26
  response_handler: ->(message) { process_response(message) unless paused? },
26
- control_handler: ->(message) { process_control(message) }
27
+ control_handler: ->(message) { process_control(message) }
27
28
  )
28
29
  end
29
30
 
@@ -33,8 +34,8 @@ module Agent99::MessageProcessing
33
34
  # @param message [Hash] The incoming message
34
35
  #
35
36
  def process_request(message)
36
- @payload = message
37
- @header = payload[:header]
37
+ @payload = message
38
+ @header = payload[:header]
38
39
  return unless validate_schema.empty?
39
40
  receive_request
40
41
  end
@@ -54,11 +55,14 @@ module Agent99::MessageProcessing
54
55
  #
55
56
  def process_control(message)
56
57
  @payload = message
57
- receive_control
58
+ if payload[:action] == 'response'
59
+ receive_response
60
+ else
61
+ receive_control
62
+ end
58
63
  end
59
64
 
60
65
 
61
-
62
66
  # Handles incoming request messages (to be overridden by subclasses).
63
67
  #
64
68
  def receive_request
@@ -78,8 +82,9 @@ module Agent99::MessageProcessing
78
82
  # @raise [StandardError] If there's an error processing the control message
79
83
  #
80
84
  def receive_control
81
- action = payload[:action]
82
- handler = CONTROL_HANDLERS[action]
85
+
86
+ action = payload[:action]
87
+ handler = Agent99::CONTROL_HANDLERS[action]
83
88
 
84
89
  if handler
85
90
  send(handler)
@@ -89,7 +94,7 @@ module Agent99::MessageProcessing
89
94
 
90
95
  rescue StandardError => e
91
96
  logger.error "Error processing control message: #{e.message}"
92
- send_control_response("Error", { error: e.message })
97
+ send_control_response({ error: e.message })
93
98
  end
94
99
 
95
100
 
@@ -108,15 +113,16 @@ module Agent99::MessageProcessing
108
113
  # @param message [String] The response message
109
114
  # @param data [Hash, nil] Additional data to include in the response
110
115
  #
111
- def send_control_response(message, data = nil)
116
+ def send_control_response(data)
112
117
  response = {
113
118
  header: return_address.merge(type: 'control'),
114
- message: message,
119
+ action: 'response',
115
120
  data: data
116
121
  }
117
122
  @message_client.publish(response)
118
123
  end
119
124
 
125
+
120
126
  # Validates the incoming message against the defined schema.
121
127
  #
122
128
  # @return [Array] An array of validation errors, empty if validation succeeds
@@ -135,6 +141,7 @@ module Agent99::MessageProcessing
135
141
  e.messages
136
142
  end
137
143
 
144
+
138
145
  # Retrieves a field from the payload or returns a default value.
139
146
  #
140
147
  # @param field [Symbol] The field to retrieve
@@ -144,6 +151,7 @@ module Agent99::MessageProcessing
144
151
  payload[field] || default(field)
145
152
  end
146
153
 
154
+
147
155
  # Returns the default value for a field from the schema.
148
156
  #
149
157
  # @param field [Symbol] The field to get the default for
@@ -35,6 +35,12 @@ class Agent99::RegistryClient
35
35
  send_request(request)
36
36
  end
37
37
 
38
+
39
+ def fetch_all_agents
40
+ request = create_request(:get, "/")
41
+ response = send_request(request)
42
+ end
43
+
38
44
  ################################################
39
45
  private
40
46
 
@@ -48,9 +54,11 @@ class Agent99::RegistryClient
48
54
  response = @http_client.request(request)
49
55
 
50
56
  handle_response(response)
57
+
51
58
  rescue JSON::ParserError => e
52
59
  logger.error "JSON parsing error: #{e.message}"
53
60
  nil
61
+
54
62
  rescue StandardError => e
55
63
  logger.error "Request error: #{e.message}"
56
64
  nil
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Agent99
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.2"
5
5
 
6
6
  def self.version = VERSION
7
7
  end
data/lib/agent99.rb CHANGED
@@ -3,6 +3,10 @@
3
3
  require 'debug_me'
4
4
  include DebugMe
5
5
 
6
+ require 'json'
7
+ require 'json_schema'
8
+ require 'securerandom'
9
+
6
10
  module Agent99; end # Establish a namespace
7
11
 
8
12
  require_relative 'agent99/base'
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: agent99
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-05 00:00:00.000000000 Z
11
+ date: 2024-12-07 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: ai_client
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: bunny
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -154,11 +140,14 @@ files:
154
140
  - Rakefile
155
141
  - docs/todo.md
156
142
  - examples/README.md
157
- - examples/hello_world.rb
158
- - examples/hello_world_client.rb
159
- - examples/hello_world_request.rb
143
+ - examples/chief_agent.rb
144
+ - examples/control.rb
145
+ - examples/diagram.dot
146
+ - examples/diagram.png
147
+ - examples/maxwell_agent86.rb
148
+ - examples/maxwell_request.rb
160
149
  - examples/registry.rb
161
- - examples/start_agents.sh
150
+ - examples/start_rabbitmq_and_registry.sh
162
151
  - lib/agent99.rb
163
152
  - lib/agent99/.irbrc
164
153
  - lib/agent99/agent_discovery.rb
@@ -1,70 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # examples/hello_world_client.rb
3
-
4
- require 'json'
5
- require 'json_schema'
6
- require 'securerandom'
7
- require_relative '../lib/agent99'
8
-
9
- class HelloWorldClient < Agent99::Base
10
- def init
11
- send_request
12
- end
13
-
14
- def send_request
15
- to_uuid = discover_agent(capability: 'greeter', how_many: 1).first[:uuid]
16
-
17
- request = build_request(
18
- to_uuid:,
19
- greeting: 'Hey',
20
- name: 'MadBomber'
21
- )
22
-
23
- result = @message_client.publish(request)
24
- logger.info "Sent request: #{request.inspect}; status? #{result.inspect}"
25
- end
26
-
27
- def build_request(
28
- to_uuid:,
29
- greeting: 'Hello',
30
- name: 'World'
31
- )
32
-
33
- {
34
- header: {
35
- type: 'request',
36
- from_uuid: @id,
37
- to_uuid:,
38
- event_uuid: SecureRandom.uuid,
39
- timestamp: Agent99::Timestamp.new.to_i
40
- },
41
- greeting:,
42
- name:
43
- }
44
- end
45
-
46
-
47
- def receive_response
48
- logger.info "Received response: #{payload.inspect}"
49
- result = payload[:result]
50
-
51
- puts
52
- puts `echo "#{result}" | boxes -d info`
53
- puts
54
-
55
- exit(0)
56
- end
57
-
58
- #####################################################
59
- private
60
-
61
- def capabilities
62
- ['hello_world_client']
63
- end
64
- end
65
-
66
-
67
- # Example usage
68
- client = HelloWorldClient.new
69
- client.run
70
-
@@ -1,20 +0,0 @@
1
- #!/bin/bash
2
-
3
- # Start the registry server
4
- echo "Starting registry server..."
5
- # Assuming the registry server is a separate process, replace with actual command
6
- # e.g., ruby registry_server.rb &
7
- # Replace 'registry_server_command' with the actual command to start your registry server
8
- # Example: ruby registry_server.rb &
9
- ./registry.rb &
10
-
11
- # Start the HelloWorld agent
12
- echo "Starting HelloWorld agent..."
13
- ./hello_world.rb &
14
-
15
- # Start the HelloWorldClient agent
16
- echo "Starting HelloWorldClient agent..."
17
- ./hello_world_client.rb &
18
-
19
- # Wait for all background processes to finish
20
- wait