Skip to content

v1.4.0

Compare
Choose a tag to compare
@operand operand released this 07 Sep 03:40
· 52 commits to main since this release
82d63c6

Summary of Changes

This release brings multiprocessing support and a few nice API improvements. Notably, I've added an implementation of synchronous messaging with the new request() method, and I've introduced logging support for development and debugging.

There are a number of breaking API changes in this release. Please read through the following summary to update your codebase.

Since this update required a rather large internal change, please beware of bugs and report any issues that you find. ❤️

Space Class Changes

Space.add()

Space.add() no longer accepts an Agent instance as an argument. Spaces will now create the Agent instance for you when adding them to the space.

An example of the updated syntax:

space.add(MyAgent, "my_agent_id")

Note how it accepts the Agent type. Any remaining parameters starting with the id are passed to the type's constructor when creating the agent.

MultiprocessSpace added

The new MultiprocessSpace class allows for better parallelism on a single host. Adding this option was the initial focus of this update. It uses the multiprocessing library for both concurrency and communication.

NativeSpace renamed to ThreadSpace

The NativeSpace class has been renamed to ThreadSpace for clarity.

AMQPSpace updated to use multiprocessing for local concurrency

Since multiprocessing is now supported, it made sense to use it rather than threading with the AMQPSpace class for local concurrency. This means that you do not need to define separate applications to achieve parallelism on a single host when using AMQP.

Agent Class Changes

Synchronous messaging support added with Agent.request()

To allow calling actions synchronously, the Agent class now includes a request() method.

For example:

try:
    return_value = self.request({
      "to": "ExampleAgent",
      "action": {
          "name": "example_action",
          "args": {
              "content": "hello"
          }
      }
    }, timeout=5)
except ActionError as e:
    print(e.message)

The Agent.request() method is a synchronous version of the send() method that allows you to call an action and receive its return value or exception synchronously.

response() and error() renamed to handle_action_value() and handle_action_error()

Additionally, these methods are no longer implemented as actions. You should remove the @action decorator from these methods when you update them.

To clarify moving forward, an action (a method decorated by @action) should be considered a public method available to other agents. Callbacks are called upon certain events, rather than being directly invoked by an agent.

In the cases of handle_action_value() and handle_action_error(), these callbacks are invoked when a previously sent action returns a value or raises an exception.

Also note that their signatures have removed the original_message_id argument and simplified to:

def handle_action_value(self, value):

and

def handle_action_error(self, error: ActionError):

See the original_message() method for how the original message may now be accessed.

Agent.current_message() is now a method

Agent.current_message() may be called during any action or action related callback to inspect the full message being processed.

Agent.original_message() added

Similar to current_message(), original_message() may be called during the handle_action_value() and handle_action_error() callbacks to inspect the original message that the current message is responding to.

The original message must have defined the meta.id field for this method to return it. Otherwise, this method will return None.

Message schema changes

Moved id field to meta.id

The id field has been moved within the meta field to be treated similarly as any other metadata.

If set, the id field may be used to correlate response messages (internal messages that return a value or exception) with the original message they correspond to. See the original_message() method for more details.

action.args is now optional

action.args can be omitted when calling an action that takes no arguments. For example, given the following action:

@action
def example_action(self):
    ...

The following message is valid:

self.send({
    "to": "ExampleAgent",
    "action": {
        "name": "example_action"
    }
})

Beware that this may cause errors if your code currently inspects messages and expects the action.args field to be present.

Additional Changes

Added Logging Support

Some basic logging has been added. Set the LOGLEVEL environment variable to see the logs.

The default setting is warning. info will show messages as they are being sent. debug shows more fine grained handling of messages as they are received and processed.

If you'd like to add more logging or make changes, please open a PR or issue.

Demo updates

  • A multiprocess example has been added.

    Run demo run multiprocess to try it out

  • The native service has been renamed to threaded

  • The request_permission callback on Host class has been bypassed temporarily.

    Asking for terminal input in a multiprocess application doesn't work well yet. A future update should address this. So for the time being, all actions on the host will be allowed.

  • Unfortunately gradio live reloading also had to be disabled. You'll have to restart the app for code changes to take effect. If either of the above changes cause problems for you, please open an issue.