agent99 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +30 -24
- data/docs/advanced_features.md +9 -4
- data/docs/agent99_framework/central_registry.md +94 -0
- data/docs/agent99_framework/message_client.md +120 -0
- data/docs/agent99_framework/registry_client.md +119 -0
- data/docs/agent_discovery.md +9 -5
- data/docs/agent_registry_processes.md +8 -2
- data/docs/api_reference.md +14 -4
- data/docs/breaking_change_v0.0.4.md +26 -0
- data/docs/what_is_an_agent.md +293 -0
- data/examples/agent_watcher.rb +5 -1
- data/examples/chief_agent.rb +17 -6
- data/examples/control.rb +16 -7
- data/examples/example_agent.rb +16 -3
- data/examples/maxwell_agent86.rb +15 -26
- data/examples/registry.rb +18 -9
- data/lib/agent99/agent_discovery.rb +4 -0
- data/lib/agent99/agent_lifecycle.rb +34 -10
- data/lib/agent99/base.rb +5 -1
- data/lib/agent99/message_processing.rb +3 -1
- data/lib/agent99/registry_client.rb +12 -11
- data/lib/agent99/tcp_message_client.rb +183 -0
- data/lib/agent99/version.rb +1 -1
- metadata +8 -2
@@ -0,0 +1,293 @@
|
|
1
|
+
# What is an Agent?
|
2
|
+
|
3
|
+
An agent is a self-contained piece of code designed to perform a specific function or service. Unlike general-purpose classes or modules, agents can be autonomous entities that focus on doing one thing well. They embody the Single Responsibility Principle (SRP) by design.
|
4
|
+
|
5
|
+
I will be using the Ruby programming language and the agent99 gem specifically to illustrate aspects of what I think good software agents should look like. I choose Ruby because:
|
6
|
+
|
7
|
+
- Ruby allows developers to create elegant and expressive AI applications.
|
8
|
+
- It promotes paradigm shifts, challenging the Python status quo in AI development.
|
9
|
+
- Developers who value clarity can find their niche in Ruby's design.
|
10
|
+
- Ruby's metaprogramming capabilities pave the way for creative AI solutions.
|
11
|
+
- Greater integration of Ruby-based frameworks makes them viable choices in the AI landscape.
|
12
|
+
- Ruby's object-oriented style leads to more modular and testable code.
|
13
|
+
- Ruby inspires a community that often prefers its syntax over Python's.
|
14
|
+
|
15
|
+
## Getting Started with Agent99
|
16
|
+
|
17
|
+
The `agent99` is a Ruby gem that serves as a framework for developing and managing software agents, allowing for the execution and communication of these agents in a distributed environment. It implements a reference protocol that supports agent registration, discovery, and messaging through a centralized registry and a messaging system like AMQP or NATS. Each agent, derived from the `Agent99::Base` class, is designed to perform specific tasks as defined by its capabilities, adhering to the Single Responsibility Principle (SRP) for enhanced maintainability and testability. The framework facilitates modular agent interactions, enabling developers to build innovative applications while taking advantage of Ruby’s expressive syntax and metaprogramming features. The library emphasizes best practices in software design, including error handling and lifecycle management for robust agent operations.
|
18
|
+
|
19
|
+
To install the Agent99 gem, simply run:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
gem install agent99
|
23
|
+
```
|
24
|
+
|
25
|
+
The [documentation](https://github.com/MadBomber/agent99/blob/main/docs/README.md) provides a comprehensive overview of the framework, but here, we will explore definitions of software agents and the Single Responsibility Principle (SRP), along with how Agent99 distinguishes itself in agent management and description.
|
26
|
+
|
27
|
+
## What Is a Reference Implementation?
|
28
|
+
|
29
|
+
The Agent99 gem implements a protocol in Ruby that can be replicated in other programming languages. This interoperability allows software agents, given they support the Agent99 protocol, to mix and match regardless of the language they were built in.
|
30
|
+
|
31
|
+
## Understanding Software Agents and the Single Responsibility Principle
|
32
|
+
|
33
|
+
Software agents and the Single Responsibility Principle (SRP) are crucial in contemporary software development. They decompose complex systems into manageable, autonomous components, while SRP promotes the creation of maintainable, testable, and adaptable systems. Utilizing both can boost code quality and nurture agility in development teams, particularly in AI, automation, and microservices contexts.
|
34
|
+
|
35
|
+
## What Are Software Agents?
|
36
|
+
|
37
|
+
In simple terms, a software agent is a designated piece of code that performs a single function effectively. Within the Agent99 framework, agents are instances of subclasses derived from the **Agent99::Base** class. These instances can be running in their own separate process or groups of instances of different Agent99 instances can run within separate Threads in a single process.
|
38
|
+
|
39
|
+
Here's a simple example of an Agent99 agent class running in an independent process:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
# File: example_agent.rb
|
43
|
+
|
44
|
+
require 'agent99'
|
45
|
+
|
46
|
+
class ExampleAgent < Agent99::Base
|
47
|
+
def info
|
48
|
+
{
|
49
|
+
name: self.class.to_s,
|
50
|
+
type: :server,
|
51
|
+
capabilities: %w[ rubber_stamp yes_man example ],
|
52
|
+
# request_schema: {}, # ExampleRequest.schema,
|
53
|
+
# response_schema: {}, # Agent99::RESPONSE.schema
|
54
|
+
# control_schema: {}, # Agent99::CONTROL.schema
|
55
|
+
# error_schema: {}, # Agent99::ERROR.schema
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def receive_request
|
60
|
+
logger.info "Example agent received request: #{payload}"
|
61
|
+
send_response(status: 'success')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
ExampleAgent.new.run
|
66
|
+
```
|
67
|
+
|
68
|
+
```base
|
69
|
+
ruby example_agent.rb
|
70
|
+
```
|
71
|
+
|
72
|
+
Each agent subclass is responsible for specific methods that define its unique capabilities and how it handles requests. The `info` method provides a comprehensive information packet about the agent. It returns a hash containing key details that are crucial for agent registration and discovery within the system. The **:capabilities** entry in the `info` packet in an Array of Strings - synonyms - for the thing that the agent does.
|
73
|
+
|
74
|
+
For a server type agent, the only methods that are required to be defined, as in the ExampleAgent class above, are its **info** and its **receive_request** methods. Everything else from initialization, registration, message dispatching, and graceful shutdown are handled by the default methods within the **Agent99::Base** class.
|
75
|
+
|
76
|
+
More complex agents will require methods like **receive_response** and **receive_control** and potentially custom implementations of **init**, **initialize**, or **fini** may be necessary for managing state or resources.
|
77
|
+
|
78
|
+
> **RoadMap:** Currently, the Agent99 implementation defines the **capabilities** value as an `Array(String)`, with plans to enhance this functionality into descriptive unstructured text akin to defining tools for functional callbacks in LLM processing using semantic search.
|
79
|
+
|
80
|
+
## The Single Responsibility Principle (SRP)
|
81
|
+
|
82
|
+
The Single Responsibility Principle, part of the SOLID principles of object-oriented design, asserts that a class or module should have only one reason to change. This means it should fulfill a single job or responsibility effectively.
|
83
|
+
|
84
|
+
### Why SRP Matters
|
85
|
+
|
86
|
+
1. **Maintainability:** Code is easier to read and modify, leading to more maintainable systems.
|
87
|
+
2. **Testability:** Isolated responsibilities facilitate independent unit testing.
|
88
|
+
3. **Flexibility:** Minimal impact on other parts of the system when modifying one responsibility, reducing the risk of bugs.
|
89
|
+
|
90
|
+
### Applying SRP in Software Development
|
91
|
+
|
92
|
+
Implementing SRP involves:
|
93
|
+
|
94
|
+
- **Identifying Responsibilities:** Break down functionalities into specific tasks; each class or module should focus on a particular duty.
|
95
|
+
- **Modular Design:** Create a loosely coupled system to enhance separation of concerns.
|
96
|
+
- **Utilizing Design Patterns:** Harness design patterns like Observer, Strategy, and Factory to ensure clear interfaces and responsibilities.
|
97
|
+
|
98
|
+
## Alignment of Agents and SRP
|
99
|
+
|
100
|
+
The notion of software agents naturally corresponds with the SRP. Each agent can be a distinct class or module that encapsulates a specific functionality. For instance:
|
101
|
+
|
102
|
+
- An order processing agent focuses solely on order management.
|
103
|
+
- A notification agent manages the sending of alerts or messages without getting involved in order processing logic.
|
104
|
+
|
105
|
+
Designing agents with SRP in mind fosters modularity and reusability, allowing changes to one agent without affecting others and supporting more robust architecture.
|
106
|
+
|
107
|
+
## Agent99 as a Reference Framework
|
108
|
+
|
109
|
+
In its current iteration, the Agent99 Framework does not differ conceptually from other microservice architecture implementations. It enables centralized registration where agents list their capabilities for other agents or applications to discover. Agent communications occur via a distributed messaging system. The agent99 Ruby gem currently uses AMQP (via the Bunny gem and the RabbitMQ broker) and the NATS-server.
|
110
|
+
|
111
|
+
### Agent Structure
|
112
|
+
|
113
|
+
Agents in Agent99 inherit from **Agent99::Base**, which offers core functionality through crucial modules:
|
114
|
+
|
115
|
+
- **HeaderManagement:** Handles message header processing.
|
116
|
+
- **AgentDiscovery:** Facilitates capability advertisement and discovery.
|
117
|
+
- **ControlActions:** Manages control messages.
|
118
|
+
- **AgentLifecycle:** Oversees agent startup and shutdown functionality.
|
119
|
+
- **MessageProcessing:** Manages message dispatch and handling.
|
120
|
+
|
121
|
+
Every agent must define its type (server, client, or hybrid) and capabilities. The framework supports three message types: requests, responses, and control messages.
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
class Agent99::Base
|
125
|
+
include Agent99::HeaderManagement
|
126
|
+
include Agent99::AgentDiscovery
|
127
|
+
include Agent99::ControlActions
|
128
|
+
include Agent99::AgentLifecycle
|
129
|
+
include Agent99::MessageProcessing
|
130
|
+
|
131
|
+
MESSAGE_TYPES = %w[request response control]
|
132
|
+
|
133
|
+
attr_reader :id, :capabilities, :name, :payload, :header, :logger, :queue
|
134
|
+
attr_accessor :registry_client, :message_client
|
135
|
+
|
136
|
+
###################################################
|
137
|
+
private
|
138
|
+
|
139
|
+
def handle_error(message, error)
|
140
|
+
logger.error "#{message}: #{error.message}"
|
141
|
+
logger.debug error.backtrace.join("\n")
|
142
|
+
end
|
143
|
+
|
144
|
+
# the final rescue block
|
145
|
+
rescue StandardError => e
|
146
|
+
handle_error("Unhandled error in Agent99::Base", e)
|
147
|
+
exit(2)
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
151
|
+
### Centralized Registry
|
152
|
+
|
153
|
+
The registry service tracks agent availability and capabilities through a **RegistryClient**. A web-based application serves as the central registry, with a Sinatra implementation found in the `examples/registry.rb` file. Its primary function is to maintain a data store of registered agents.
|
154
|
+
|
155
|
+
It supports three core operations:
|
156
|
+
|
157
|
+
#### 1. Register
|
158
|
+
|
159
|
+

|
160
|
+
|
161
|
+
Agents register by providing their information (e.g., name and capabilities) to the registry service. Here's how registration works in practice:
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
class WeatherAgent < Agent99::Base
|
165
|
+
TYPE = :server
|
166
|
+
|
167
|
+
def capabilities
|
168
|
+
%w[get_temperature get_forecast]
|
169
|
+
end
|
170
|
+
|
171
|
+
def receive_request(message)
|
172
|
+
case message.payload[:action]
|
173
|
+
when 'get_temperature'
|
174
|
+
send_response({ temperature: 72, unit: 'F' })
|
175
|
+
when 'get_forecast'
|
176
|
+
send_response({ forecast: 'Sunny with a chance of rain' })
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Start the agent
|
182
|
+
WeatherAgent.new.run
|
183
|
+
```
|
184
|
+
|
185
|
+
Upon successful registration, agents receive a universally unique ID (UUID) that identifies them in the system. The registration process is handled automatically by the Agent99 framework when you call `run`.
|
186
|
+
|
187
|
+
#### 2. Discover
|
188
|
+
|
189
|
+
Agents can query the registry to discover capabilities. The discovery operation retrieves information about agents offering specific capabilities via an HTTP GET request.
|
190
|
+
|
191
|
+
#### 3. Withdraw
|
192
|
+
|
193
|
+
When an agent needs to exit the system, it withdraws its registration using its UUID, removing it from the available agents list through an HTTP DELETE request.
|
194
|
+
|
195
|
+
### Messaging Network
|
196
|
+
|
197
|
+
The Ruby implementation of Agent99 currently focuses on AMQP messaging systems. Messages are formatted as JSON structures that adhere to defined schemas, allowing the **MessageClient** to validate messages effortlessly.
|
198
|
+
|
199
|
+
Messages are validated against defined schemas, and invalid messages return to the sender without invoking agent-specific processes.
|
200
|
+
|
201
|
+
Message types within the framework include:
|
202
|
+
|
203
|
+
#### Request Messages
|
204
|
+
|
205
|
+
These messages are validated against an agent-defined schema and include:
|
206
|
+
|
207
|
+
- A header with routing information.
|
208
|
+
- Agent-specific elements with their types and examples.
|
209
|
+
|
210
|
+
Requests are handled by the `receive_request` handler in target agents.
|
211
|
+
|
212
|
+
Here's an example of a request message schema using the [SimpleJsonSchemaBuilder gem](https://github.com/mooktakim/simple_json_schema_builder)
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
# examples/maxwell_request.rb
|
216
|
+
|
217
|
+
require 'agent99/header_schema'
|
218
|
+
|
219
|
+
class MaxwellRequest < SimpleJsonSchemaBuilder::Base
|
220
|
+
object do
|
221
|
+
object :header, schema: Agent99::HeaderSchema
|
222
|
+
|
223
|
+
string :greeting, required: false, examples: ["Hello"]
|
224
|
+
string :name, required: true, examples: ["World"]
|
225
|
+
end
|
226
|
+
end
|
227
|
+
```
|
228
|
+
|
229
|
+
This schema defines a `MaxwellRequest` with a header (using the `Agent99::HeaderSchema`), an optional greeting, and a required name. A valid JSON message conforming to this schema might look like:
|
230
|
+
|
231
|
+
```json
|
232
|
+
{
|
233
|
+
"header": {
|
234
|
+
"from_uuid": "123e4567-e89b-12d3-a456-426614174000",
|
235
|
+
"to_uuid": "987e6543-e21b-12d3-a456-426614174000",
|
236
|
+
"message_id": "msg-001",
|
237
|
+
"correlation_id": "corr-001",
|
238
|
+
"timestamp": "2023-04-01T12:00:00Z"
|
239
|
+
},
|
240
|
+
"greeting": "Hello",
|
241
|
+
"name": "Agent99"
|
242
|
+
}
|
243
|
+
```
|
244
|
+
|
245
|
+
Using such schemas ensures that messages are well-structured and contain all necessary information before being processed by agents.
|
246
|
+
|
247
|
+
Here is how the **MaxwellAgent** associates itself with its specific request schema:
|
248
|
+
|
249
|
+
```ruby
|
250
|
+
# examples/maxwell_agent86.rb
|
251
|
+
|
252
|
+
require 'agent99'
|
253
|
+
require_relative 'maxwell_request'
|
254
|
+
|
255
|
+
class MaxwellAgent86 < Agent99::Base
|
256
|
+
def info
|
257
|
+
{
|
258
|
+
# ...
|
259
|
+
request_schema: MaxwellRequest.schema,
|
260
|
+
# ...
|
261
|
+
}
|
262
|
+
end
|
263
|
+
```
|
264
|
+
|
265
|
+
When an agent subclass defines a **:request_schema** in its `info`, the message processing of the **Agent99::Base** validates all incoming requests against the schema. If there are errors, those errors are returned to the sender without presenting the request message to the agent's custom **receive_request** method.
|
266
|
+
|
267
|
+
#### Response Messages
|
268
|
+
|
269
|
+
Responses are routed back to the requesting agent and include:
|
270
|
+
|
271
|
+
- The original message header in reverse.
|
272
|
+
- The response payload.
|
273
|
+
- Status information.
|
274
|
+
|
275
|
+
Responses are processed by the `receive_response` method.
|
276
|
+
|
277
|
+
#### Control Messages
|
278
|
+
|
279
|
+
Control messages manage agent lifecycles and configurations and include commands such as:
|
280
|
+
|
281
|
+
- **shutdown:** Stop the agent.
|
282
|
+
- **pause/resume:** Temporarily suspend or resume operations.
|
283
|
+
- **update_config:** Modify agent configurations.
|
284
|
+
- **status:** Query agent state.
|
285
|
+
- **response:** Handle control operation results.
|
286
|
+
|
287
|
+
Messages are queued with a 60-second TTL (Time To Live) to prevent buildup from inactive agents.
|
288
|
+
|
289
|
+
## Additional Resources
|
290
|
+
|
291
|
+
For further exploration, check out the documentation of the current Ruby implementation at [GitHub](https://github.com/MadBomber/agent99).
|
292
|
+
|
293
|
+
Contributions to this initial Ruby reference implementation are welcome! It would be exciting to see additional language implementations.
|
data/examples/agent_watcher.rb
CHANGED
@@ -28,7 +28,11 @@ require_relative '../lib/agent99'
|
|
28
28
|
class AgentWatcher < Agent99::Base
|
29
29
|
TYPE = :client
|
30
30
|
|
31
|
-
def capabilities =
|
31
|
+
def capabilities = {
|
32
|
+
info: {
|
33
|
+
capabilities: %w[ launch_agents watcher launcher ]
|
34
|
+
}
|
35
|
+
}
|
32
36
|
|
33
37
|
def init
|
34
38
|
@watch_path = ENV.fetch('AGENT_WATCH_PATH', './agents')
|
data/examples/chief_agent.rb
CHANGED
@@ -14,7 +14,23 @@
|
|
14
14
|
require_relative '../lib/agent99'
|
15
15
|
|
16
16
|
class ChiefAgent < Agent99::Base
|
17
|
-
|
17
|
+
# this information is made available when the agent
|
18
|
+
# registers with the central registry service. It is
|
19
|
+
# made available during the discovery process.
|
20
|
+
#
|
21
|
+
def info
|
22
|
+
{
|
23
|
+
name: self.class.to_s,
|
24
|
+
type: :client,
|
25
|
+
capabilities: ['Chief of Control'],
|
26
|
+
# request_schema: ChiefRequest.schema,
|
27
|
+
# response_schema: {}, # Agent99::RESPONSE.schema
|
28
|
+
# control_schema: {}, # Agent99::CONTROL.schema
|
29
|
+
# error_schema: {}, # Agent99::ERROR.schema
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
|
18
34
|
|
19
35
|
# init is called at the end of the initialization process.
|
20
36
|
# It may be only something that a :client type agent would do.
|
@@ -82,11 +98,6 @@ class ChiefAgent < Agent99::Base
|
|
82
98
|
|
83
99
|
exit(0)
|
84
100
|
end
|
85
|
-
|
86
|
-
|
87
|
-
def capabilities
|
88
|
-
['Chief of Control']
|
89
|
-
end
|
90
101
|
end
|
91
102
|
|
92
103
|
|
data/examples/control.rb
CHANGED
@@ -4,7 +4,21 @@
|
|
4
4
|
require_relative '../lib/agent99'
|
5
5
|
|
6
6
|
class Control < Agent99::Base
|
7
|
-
|
7
|
+
# this information is made available when the agent
|
8
|
+
# registers with the central registry service. It is
|
9
|
+
# made available during the discovery process.
|
10
|
+
#
|
11
|
+
def info
|
12
|
+
{
|
13
|
+
name: self.class.to_s,
|
14
|
+
type: :hybrid,
|
15
|
+
capabilities: ['control', 'headquarters', 'secret underground base'],
|
16
|
+
# request_schema: ControlRequest.schema,
|
17
|
+
# response_schema: {}, # Agent99::RESPONSE.schema
|
18
|
+
# control_schema: {}, # Agent99::CONTROL.schema
|
19
|
+
# error_schema: {}, # Agent99::ERROR.schema
|
20
|
+
}
|
21
|
+
end
|
8
22
|
|
9
23
|
attr_accessor :statuses
|
10
24
|
|
@@ -14,11 +28,6 @@ class Control < Agent99::Base
|
|
14
28
|
end
|
15
29
|
|
16
30
|
|
17
|
-
def capabilities
|
18
|
-
['control', 'headquarters', 'secret underground base']
|
19
|
-
end
|
20
|
-
|
21
|
-
|
22
31
|
def send_control_message(message:, payload: {})
|
23
32
|
@agents.each do |agent|
|
24
33
|
response = @message_client.publish(
|
@@ -48,7 +57,7 @@ class Control < Agent99::Base
|
|
48
57
|
|
49
58
|
|
50
59
|
def stop_all
|
51
|
-
send_control_message(message: '
|
60
|
+
send_control_message(message: 'shutdown')
|
52
61
|
end
|
53
62
|
|
54
63
|
def get_all_status
|
data/examples/example_agent.rb
CHANGED
@@ -15,9 +15,22 @@
|
|
15
15
|
require_relative '../../lib/agent99'
|
16
16
|
|
17
17
|
class ExampleAgent < Agent99::Base
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
# this information is made available when the agent
|
19
|
+
# registers with the central registry service. It is
|
20
|
+
# made available during the discovery process.
|
21
|
+
#
|
22
|
+
def info
|
23
|
+
{
|
24
|
+
name: self.class.to_s,
|
25
|
+
type: :server,
|
26
|
+
capabilities: %w[ rubber_stamp yes_man example ],
|
27
|
+
# request_schema: {}, # ExampleRequest.schema,
|
28
|
+
# response_schema: {}, # Agent99::RESPONSE.schema
|
29
|
+
# control_schema: {}, # Agent99::CONTROL.schema
|
30
|
+
# error_schema: {}, # Agent99::ERROR.schema
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
21
34
|
|
22
35
|
def receive_request
|
23
36
|
logger.info "Example agent received request: #{payload}"
|
data/examples/maxwell_agent86.rb
CHANGED
@@ -10,12 +10,21 @@ require_relative '../lib/agent99'
|
|
10
10
|
require_relative 'maxwell_request'
|
11
11
|
|
12
12
|
class MaxwellAgent86 < Agent99::Base
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
#
|
17
|
-
|
18
|
-
|
13
|
+
# this information is made available when the agent
|
14
|
+
# registers with the central registry service. It is
|
15
|
+
# made available during the discovery process.
|
16
|
+
#
|
17
|
+
def info
|
18
|
+
{
|
19
|
+
name: self.class.to_s,
|
20
|
+
type: :server,
|
21
|
+
capabilities: %w[ greeter hello_world hello-world hello],
|
22
|
+
request_schema: MaxwellRequest.schema,
|
23
|
+
# response_schema: {}, # Agent99::RESPONSE.schema
|
24
|
+
# control_schema: {}, # Agent99::CONTROL.schema
|
25
|
+
# error_schema: {}, # Agent99::ERROR.schema
|
26
|
+
}
|
27
|
+
end
|
19
28
|
|
20
29
|
#######################################
|
21
30
|
private
|
@@ -90,26 +99,6 @@ class MaxwellAgent86 < Agent99::Base
|
|
90
99
|
def receive_response(response)
|
91
100
|
loger.warn("Unexpected response type message: response.inspect")
|
92
101
|
end
|
93
|
-
|
94
|
-
|
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.
|
107
|
-
#
|
108
|
-
# For now, lets just go with the Array of Strings.
|
109
|
-
#
|
110
|
-
def capabilities
|
111
|
-
%w[ greeter hello_world hello-world hello]
|
112
|
-
end
|
113
102
|
end
|
114
103
|
|
115
104
|
# Example usage
|
data/examples/registry.rb
CHANGED
@@ -27,13 +27,22 @@ end
|
|
27
27
|
# Endpoint to register an agent
|
28
28
|
post '/register' do
|
29
29
|
request.body.rewind
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
30
|
+
request_data = JSON.parse(request.body.read, symbolize_names: true)
|
31
|
+
debug_me{[
|
32
|
+
:request_data
|
33
|
+
]}
|
34
|
+
agent_name = request_data[:name]
|
35
|
+
agent_uuid = SecureRandom.uuid
|
36
|
+
|
37
|
+
debug_me{[
|
38
|
+
:agent_name,
|
39
|
+
:agent_uuid
|
40
|
+
]}
|
41
|
+
|
42
|
+
# Ensure capabilities are lowercase
|
43
|
+
request_data[:capabilities].map!{|c| c.downcase}
|
44
|
+
|
45
|
+
AGENT_REGISTRY << request_data.merge({uuid: agent_uuid})
|
37
46
|
|
38
47
|
status 201
|
39
48
|
content_type :json
|
@@ -47,7 +56,7 @@ get '/discover' do
|
|
47
56
|
capability = params['capability'].downcase
|
48
57
|
|
49
58
|
matching_agents = AGENT_REGISTRY.select do |agent|
|
50
|
-
agent[:capabilities]
|
59
|
+
agent[:capabilities]&.include?(capability)
|
51
60
|
end
|
52
61
|
|
53
62
|
content_type :json
|
@@ -79,4 +88,4 @@ end
|
|
79
88
|
# Start the Sinatra server
|
80
89
|
if __FILE__ == $PROGRAM_NAME
|
81
90
|
Sinatra::Application.run!
|
82
|
-
end
|
91
|
+
end
|
@@ -11,16 +11,19 @@ module Agent99::AgentLifecycle
|
|
11
11
|
def initialize(registry_client: Agent99::RegistryClient.new,
|
12
12
|
message_client: Agent99::AmqpMessageClient.new,
|
13
13
|
logger: Logger.new($stdout))
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@
|
17
|
-
@
|
18
|
-
@
|
19
|
-
@
|
20
|
-
@
|
14
|
+
@agents = {}
|
15
|
+
@payload = nil
|
16
|
+
@name = self.class.name
|
17
|
+
@capabilities = capabilities
|
18
|
+
@id = nil
|
19
|
+
@registry_client = registry_client
|
20
|
+
@message_client = message_client
|
21
|
+
@logger = logger
|
22
|
+
|
23
|
+
validate_info_keys
|
21
24
|
|
22
25
|
@registry_client.logger = logger
|
23
|
-
register
|
26
|
+
register(info)
|
24
27
|
|
25
28
|
@queue = message_client.setup(agent_id: id, logger:)
|
26
29
|
|
@@ -29,12 +32,33 @@ module Agent99::AgentLifecycle
|
|
29
32
|
setup_signal_handlers
|
30
33
|
end
|
31
34
|
|
35
|
+
|
36
|
+
def validate_info_keys
|
37
|
+
required_keys = [:name, :capabilities]
|
38
|
+
if respond_to? :info
|
39
|
+
missing_keys = required_keys - info.keys
|
40
|
+
unless missing_keys.empty?
|
41
|
+
logger.error <<~MESSAGE
|
42
|
+
This agent's info method is missing
|
43
|
+
#{1 == missing_keys.size ? 'a required key' : 'some required keys'}:
|
44
|
+
#{missing_keys}
|
45
|
+
MESSAGE
|
46
|
+
.split("\n").join
|
47
|
+
exit(1)
|
48
|
+
end
|
49
|
+
else
|
50
|
+
logger.error "An agent must implement the info method"
|
51
|
+
exit(1)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
32
56
|
# Registers the agent with the registry service.
|
33
57
|
#
|
34
58
|
# @raise [StandardError] If registration fails
|
35
59
|
#
|
36
|
-
def register
|
37
|
-
@id = registry_client.register(
|
60
|
+
def register(agent_info)
|
61
|
+
@id = registry_client.register(info: agent_info)
|
38
62
|
logger.info "Registered Agent #{name} with ID: #{id}"
|
39
63
|
rescue StandardError => e
|
40
64
|
handle_error("Error during registration", e)
|
data/lib/agent99/base.rb
CHANGED
@@ -37,7 +37,11 @@ class Agent99::Base
|
|
37
37
|
|
38
38
|
MESSAGE_TYPES = %w[request response control]
|
39
39
|
|
40
|
-
attr_reader :id, :capabilities, :name
|
40
|
+
attr_reader :id, :capabilities, :name
|
41
|
+
attr_reader :payload, :header, :queue
|
42
|
+
attr_reader :logger
|
43
|
+
attr_reader :agents
|
44
|
+
|
41
45
|
attr_accessor :registry_client, :message_client
|
42
46
|
|
43
47
|
|
@@ -128,7 +128,9 @@ module Agent99::MessageProcessing
|
|
128
128
|
# @return [Array] An array of validation errors, empty if validation succeeds
|
129
129
|
#
|
130
130
|
def validate_schema
|
131
|
-
|
131
|
+
return unless info[:request_schema]
|
132
|
+
|
133
|
+
schema = JsonSchema.parse!(info[:request_schema])
|
132
134
|
schema.expand_references!
|
133
135
|
validator = JsonSchema::Validator.new(schema)
|
134
136
|
|
@@ -8,17 +8,18 @@ class Agent99::RegistryClient
|
|
8
8
|
attr_accessor :logger
|
9
9
|
|
10
10
|
def initialize(
|
11
|
-
base_url: ENV.fetch('
|
11
|
+
base_url: ENV.fetch('AGENT99_REGISTRY_URL', 'http://localhost:4567'),
|
12
12
|
logger: Logger.new($stdout)
|
13
13
|
)
|
14
|
-
@base_url
|
15
|
-
@logger
|
16
|
-
@http_client
|
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
17
|
end
|
18
18
|
|
19
|
-
def register(
|
20
|
-
|
21
|
-
|
19
|
+
def register(info:)
|
20
|
+
payload = info
|
21
|
+
request = create_request(:post, "/register", payload)
|
22
|
+
@id = send_request(request)
|
22
23
|
end
|
23
24
|
|
24
25
|
def withdraw(id)
|
@@ -37,16 +38,16 @@ class Agent99::RegistryClient
|
|
37
38
|
|
38
39
|
|
39
40
|
def fetch_all_agents
|
40
|
-
request
|
41
|
-
response
|
41
|
+
request = create_request(:get, "/")
|
42
|
+
response = send_request(request)
|
42
43
|
end
|
43
44
|
|
44
45
|
################################################
|
45
46
|
private
|
46
47
|
|
47
48
|
def create_request(method, path, body = nil)
|
48
|
-
request
|
49
|
-
request.body
|
49
|
+
request = Object.const_get("Net::HTTP::#{method.capitalize}").new(path, { "Content-Type" => "application/json" })
|
50
|
+
request.body = body.to_json if body
|
50
51
|
request
|
51
52
|
end
|
52
53
|
|